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/directives.md CHANGED
@@ -129,6 +129,7 @@ See [relationships.md](relationships.md) for the full guide.
129
129
  | `@belongsTo` | inverse of has-one/has-many |
130
130
  | `@belongsToMany` | many-to-many: a list, or paginated with `type:` |
131
131
  | `@morphTo`, `@morphOne`, `@morphMany`, `@morphToMany` | the polymorphic variants |
132
+ | `@hasManyThrough`, `@hasOneThrough` | PHP Lighthouse's names for `@hasMany` / `@hasOne` on a `has_many` / `has_one :through` |
132
133
 
133
134
  ```graphql
134
135
  type User {
@@ -142,6 +143,28 @@ type User {
142
143
  The relation name defaults to the field name (snake_cased). Override with
143
144
  `@hasMany(relation: "authored_posts")`.
144
145
 
146
+ ### `@with`, `@withCount` and `@lazyLoad`
147
+
148
+ Eager loading for fields the relationship directives do not resolve, each across
149
+ every parent in the response at once. See
150
+ [relationships.md](relationships.md#eager-loading-for-other-fields).
151
+
152
+ ```graphql
153
+ type User {
154
+ "@with loads the relation before the field resolves, for a method that reads it."
155
+ taskSummary: String! @with(relation: "tasks") @method(name: "task_summary")
156
+
157
+ "@withCount sets posts_count on the user; the field's resolver reads it."
158
+ activity: String! @withCount(relation: "posts", scopes: ["published"]) @method
159
+
160
+ "@lazyLoad loads relations of the records the field returns."
161
+ posts: [Post!]! @hasMany @lazyLoad(relations: ["tags", "comments.author"])
162
+ }
163
+ ```
164
+
165
+ `relation` / `relations` may be nested with dots. `scopes` narrow the loaded or
166
+ counted records. `@with`'s `relation` defaults to the field's name.
167
+
145
168
  ---
146
169
 
147
170
  ## Field directives
@@ -162,10 +185,16 @@ type Contact {
162
185
  ```
163
186
 
164
187
  On `Query` and `Mutation` the name is looked up in the queries / mutations
165
- namespaces, elsewhere in [`resolver_namespaces`](configuration.md). The class may
188
+ namespaces, a nested name too (`Feeds::Home` is `Resolvers::Feeds::Home`), and
189
+ then as written. On any other type it is looked up in the resolvers namespace the
190
+ same way (`Posts::TagsCount` is `Resolvers::Posts::TagsCount`), then in
191
+ [`resolver_namespaces`](configuration.md). There, the class's `object` is the parent: the
192
+ record the field belongs to (see
193
+ [resolvers.md](resolvers.md#fields-of-other-types)). The class may
166
194
  be a `GraphQL::Schema::Resolver` (graphql-ruby's contract), or a plain callable —
167
195
  a class-level `call`, an instance `#call`, or an instance `#resolve`, each
168
- receiving `(object, args, context)`:
196
+ receiving `(object, args, context)`, and the [`ResolveInfo`](resolvers.md#resolveinfo)
197
+ as a fourth argument when it takes one:
169
198
 
170
199
  ```ruby
171
200
  module Resolvers
@@ -177,6 +206,17 @@ module Resolvers
177
206
  end
178
207
  ```
179
208
 
209
+ A method after `@` replaces `call`, as PHP Lighthouse's `Class@method`: it is called
210
+ on the class when the class has it, else on an instance. On a
211
+ `GraphQL::Schema::Resolver` it replaces `resolve`, and `ready?` / `authorized?`
212
+ still run.
213
+
214
+ ```graphql
215
+ type Query {
216
+ usersTotal: Int @field(resolver: "Statistics@users_total") # Resolvers::Statistics.users_total
217
+ }
218
+ ```
219
+
180
220
  ### `@rename`
181
221
 
182
222
  Read a differently-named attribute off the parent object. This is the explicit
@@ -191,15 +231,164 @@ type User {
191
231
  ### `@method`
192
232
 
193
233
  Resolve a field by calling a method on the parent object (PHP-parity). Great for
194
- exposing a computed method or predicate without a custom resolver.
234
+ exposing a computed method or predicate without a custom resolver. `name:`
235
+ defaults to the field's snake_case name.
195
236
 
196
237
  ```graphql
197
238
  type Conversation {
198
239
  labels: [String!]! @method(name: "label_list")
199
240
  canReply: Boolean @method(name: "can_reply?")
241
+ summary(words: Int = 20): String! @method
242
+ }
243
+ ```
244
+
245
+ The field's arguments reach the method: as snake_case keywords when it takes
246
+ keywords (`def summary(words:)`), otherwise positionally, in the order the SDL
247
+ declares them, as PHP Lighthouse passes them (`def summary(words)`). The SDL's
248
+ defaults apply, and a method taking no parameters is called without them.
249
+
250
+ ## Relay
251
+
252
+ ### `@globalId` and `@node`
253
+
254
+ `@globalId` encodes a field's value as a Relay global id (base64 of `Type:id`), and
255
+ decodes an argument or input field (`decode: ARRAY | TYPE | ID`). `@node` on a type
256
+ makes it implement `Node` and adds `Query.node(id:)`, which finds the type's model
257
+ record, or calls `@node(resolver:)`. See [relay.md](relay.md).
258
+
259
+ ```graphql
260
+ type User @node {
261
+ id: ID! @globalId
262
+ }
263
+
264
+ type Query {
265
+ user(id: ID! @globalId(decode: ID) @eq): User @find
266
+ }
267
+ ```
268
+
269
+ ## Execution
270
+
271
+ ### `@namespaced`
272
+
273
+ A field that only groups others: it resolves to `true`, and the fields of its type
274
+ resolve with their own directives. On `Query` or `Mutation`.
275
+
276
+ ```graphql
277
+ type Query { post: PostQueries! @namespaced }
278
+ type PostQueries {
279
+ find(id: ID! @eq): Post @find
280
+ list: [Post!]! @paginate
281
+ }
282
+ ```
283
+
284
+ ### `@throttle`
285
+
286
+ Rails' `rate_limit` on one field: at most `maxAttempts` calls (its `to:`) every
287
+ `decayMinutes` (its `within:`, default 60 a minute), counted before anything else runs.
288
+ It takes `rate_limit`'s options, and counts as `rate_limit` does: in the controller's
289
+ `cache_store`, under the same key, and instrumented as `rate_limit.action_controller`.
290
+ See [security.md](security.md#rate-limiting).
291
+
292
+ | Argument | `rate_limit`'s | Default |
293
+ | --- | --- | --- |
294
+ | `by:` | `by:`, who the client is: a path in the context, as [`@inject`](resolvers.md#spread-and-inject)'s (`"current_user.id"`) | the client's IP |
295
+ | `with:` | `with:`, the controller's method to call past the limit | `rate_limit`'s: a 429 (it raises `ActionController::TooManyRequests`) |
296
+ | `name:` | `name:`; fields, and the controller's own `rate_limit`, of the same name count together | the field (`Query.x`) |
297
+ | `prefix:` | `scope:` | the controller's path |
298
+
299
+ ```graphql
300
+ type Mutation {
301
+ login(email: String!, password: String!): User @throttle(maxAttempts: 5)
302
+ createTag(name: String!): Tag! @throttle(maxAttempts: 20, by: "current_user.id", with: "too_many_requests") @guard @create
200
303
  }
201
304
  ```
202
305
 
306
+ ### `@async`
307
+
308
+ Runs a mutation in a job and answers `true` at once. `@guard`, the `@can*` family,
309
+ `@validator` and the argument directives run in the request, so the client still
310
+ gets their errors; what resolves the field runs in
311
+ `Lighthouse::GraphQL::AsyncMutationJob` (ActiveJob), with the arguments and the
312
+ context values ActiveJob can serialize (records too, through GlobalID). The field
313
+ must be a `Mutation` field returning `Boolean`, and the schema a named constant.
314
+
315
+ ```graphql
316
+ type Mutation {
317
+ importFeed(url: String!): Boolean! @guard @async(queue: "imports")
318
+ }
319
+ ```
320
+
321
+ ### `@hide` and `@show`
322
+
323
+ Leave a field out of the schema by environment (`Rails.env`): `@hide(env:)` removes
324
+ it in the environments listed, `@show(env:)` keeps it only in those.
325
+
326
+ ```graphql
327
+ type Query {
328
+ debugInfo: String @show(env: ["development", "test"])
329
+ legacyFeed: [Post!]! @hide(env: ["production"]) @all
330
+ }
331
+ ```
332
+
333
+ ## Type directives
334
+
335
+ ### `@model`
336
+
337
+ Maps an object type to the ActiveRecord model whose records it resolves, when the
338
+ names differ, as PHP Lighthouse's. Every directive that takes `model:` (`@all`,
339
+ `@find`, `@first`, `@paginate`, `@create`, `@update`, `@upsert`, `@delete`, the
340
+ `@can*` family...) on a field returning the type gets it, unless it names its own; a
341
+ union or interface resolves the model's records to the type; `@node` finds them. The
342
+ schema does not build unless the class is an ActiveRecord model.
343
+
344
+ ```graphql
345
+ type Article @model(class: "Post") { id: ID! title: String! }
346
+
347
+ type Query {
348
+ articles: [Article!]! @paginate # pages through Post
349
+ article(id: ID! @eq): Article @find
350
+ }
351
+ ```
352
+
353
+ ### `@enum`
354
+
355
+ The internal value of an enum value: what a resolver gets for it in an argument,
356
+ and what a result answers with it for.
357
+
358
+ ```graphql
359
+ enum Role {
360
+ ADMIN @enum(value: "admin")
361
+ MEMBER @enum(value: "member")
362
+ }
363
+
364
+ type Query { users(role: Role @eq): [User!]! @all } # filters on "admin"
365
+ ```
366
+
367
+ They go on a union, an interface or a scalar, and name the class that resolves or
368
+ coerces its values. Without them, Lighthouse uses the type's class
369
+ (`Types::<Name>Type` including `Lighthouse::GraphQL::Type`), or its default. See
370
+ [types.md](types.md).
371
+
372
+ ### `@union` and `@interface`
373
+
374
+ Name the class that picks the object type of each value: `resolve_type(object,
375
+ context)` or `call(object, context)`, returning a type name or a type class.
376
+ Looked up in the types namespace (nested names too), then as written.
377
+
378
+ ```graphql
379
+ union SearchResult @union(resolveType: "SearchResultResolver") = Post | User
380
+ interface Authored @interface(resolveType: "AuthoredResolver") { id: ID! }
381
+ ```
382
+
383
+ ### `@scalar`
384
+
385
+ Names the class that reads and writes the scalar's values: `coerce_input(value,
386
+ context)` and `coerce_result(value, context)`, as a `GraphQL::Schema::Scalar`.
387
+
388
+ ```graphql
389
+ scalar DateTime @scalar(class: "Types::DateTimeType")
390
+ ```
391
+
203
392
  ---
204
393
 
205
394
  ## Authentication & Authorization
@@ -263,7 +452,40 @@ type Query {
263
452
  ```
264
453
 
265
454
  Builders are looked up in their own namespace, `Builders::` (app/graphql/builders)
266
- — they are not resolvers. See [resolvers.md](resolvers.md#builders).
455
+ — they are not resolvers. See [resolvers.md](resolvers.md#builders). The class is
456
+ `class:` with `method:` (default `call`), or PHP Lighthouse's `method: "Class@method"`.
457
+
458
+ On a field, the method gets `(relation, args, context)`, or the directive's
459
+ `value:` in place of the arguments. On an argument or an input field, it gets
460
+ `(relation, value, context)` whenever the client sends that argument, an explicit
461
+ `null` included, and not when it is left out:
462
+
463
+ ```graphql
464
+ type Query {
465
+ posts(since: DateTime @builder(method: "Recent@since")): [Post!]! @all
466
+ latest: [Post!]! @all @builder(class: "Recent", value: 5)
467
+ }
468
+ ```
469
+
470
+ ```ruby
471
+ class Builders::Recent
472
+ def self.call(relation, count, _context) = relation.order(created_at: :desc).limit(count)
473
+ def self.since(relation, time, _context) = time ? relation.where(created_at: time..) : relation
474
+ end
475
+ ```
476
+
477
+ ### `@whereAuth`
478
+
479
+ Keep only the records the signed-in user owns, through a relation of the model: a
480
+ `belongs_to`, a `has_many`, a `:through`. A guest gets none. It applies where
481
+ `@builder` does (`@all`, `@paginate`, `@first`, `@count`, `enhance_builder`).
482
+
483
+ ```graphql
484
+ type Query {
485
+ myPosts: [Post!]! @paginate @whereAuth(relation: "author")
486
+ myTeams: [Team!]! @all @whereAuth(relation: "members")
487
+ }
488
+ ```
267
489
 
268
490
  ```ruby
269
491
  module Builders
@@ -292,6 +514,10 @@ Full guide: [filtering-and-ordering.md](filtering-and-ordering.md).
292
514
  | `@like` | `column LIKE %value%` |
293
515
  | `@whereKey` | `primary key = value` (`IN` for a list) |
294
516
  | `@where(operator:)` | generic operator (`GT`, `GTE`, `IN`, `IS_NULL`, `CONTAINS`, …) |
517
+ | `@whereNull` / `@whereNotNull` | `true`: `column IS NULL` / `IS NOT NULL`; `false`: the opposite |
518
+ | `@whereBetween` / `@whereNotBetween` | `column BETWEEN` / `NOT BETWEEN` the input's two fields, in order |
519
+ | `@whereJsonContains` | a JSON column contains the value (every value of a list); `key: "meta->tags"` points into it |
520
+ | `@scope(name:)` | calls the model scope with the value (`name:` defaults to the argument's) |
295
521
  | `@orderBy` | sort by one or more columns |
296
522
 
297
523
  ```graphql
@@ -307,6 +533,41 @@ type Query {
307
533
 
308
534
  The column defaults to the snake_cased argument name; override with `key:`.
309
535
 
536
+ ```graphql
537
+ type Query {
538
+ posts(
539
+ unpublished: Boolean @whereNull(key: "published_at")
540
+ createdAt: DateRange @whereBetween(key: "created_at")
541
+ tagged: [String!] @whereJsonContains(key: "tags")
542
+ trending: Boolean @scope
543
+ filter: PostFilter @nest
544
+ limit: Int @limit
545
+ ): [Post!]! @all
546
+ }
547
+
548
+ input DateRange { from: DateTime! to: DateTime! }
549
+ input PostFilter { title: String @like, authorId: ID @eq(key: "author_id") }
550
+ ```
551
+
552
+ ### `@nest`
553
+
554
+ On an argument whose type is an input object, applies the filters of the input's
555
+ fields (at any depth, for those that carry `@nest` too), as if they were the
556
+ field's own arguments.
557
+
558
+ ### `@limit`
559
+
560
+ On an `Int` argument, caps how many results the field returns at the client's
561
+ value; `null` leaves them alone. A query not run yet gets a `LIMIT`; the records of
562
+ a relation, loaded for every parent at once, are cut per parent. A negative value is
563
+ an error.
564
+
565
+ `@limit(builder: true)` puts the `LIMIT` on the field's query instead, with the
566
+ other argument directives. Use it on root fields: on a relation batched for many
567
+ parents it would cut them all together, as PHP Lighthouse warns. PHP Lighthouse
568
+ also allows `@limit` on the field itself, where it only marks the field the argument
569
+ limits; it is accepted there and changes nothing.
570
+
310
571
  ### `@whereConditions` / `@whereHasConditions`
311
572
 
312
573
  Give clients a structured, dynamic `WHERE` builder (AND/OR/operators, and HAS for
@@ -319,6 +580,35 @@ type Query {
319
580
  }
320
581
  ```
321
582
 
583
+ ### `@trim`, `@hash`, `@drop`, `@bind` and `@upload`
584
+
585
+ Rewrite an argument's or input field's value before the field resolves, in the
586
+ order the SDL lists them:
587
+
588
+ | Directive | The resolver gets |
589
+ | --- | --- |
590
+ | `@trim` | the string without leading and trailing whitespace. On a field: every string it takes, nested inputs included |
591
+ | `@drop` | nothing: the argument is removed |
592
+ | `@hash` | the value hashed with BCrypt (the `bcrypt` gem, as `has_secure_password`) |
593
+ | `@bind(class:, column: "id", with: [], optional: false)` | the model record(s) the value identifies, eager-loading `with`; or what a class's `call(value, context)` returns |
594
+ | `@upload(disk:, path: "/", public: false)` | the signed id of the ActiveStorage blob the file was stored as, on the app's service `disk` names ([uploads.md](uploads.md)) |
595
+
596
+ ```graphql
597
+ type Mutation {
598
+ signUp(email: String! @trim, password: String! @hash, captcha: String @drop): User! @create
599
+ assign(task: ID! @bind(class: "Task"), user: ID! @bind(class: "User", with: ["roles"])): Task!
600
+ updateAvatar(avatar: Upload! @upload(path: "avatars")): User! @update @inject(context: "current_user.id", name: "id")
601
+ }
602
+ ```
603
+
604
+ `@bind` fails the field with a validation error on the argument (`The selected task
605
+ is invalid.`) when no record matches, unless `optional: true`, which passes `null`.
606
+
607
+ `@globalId`, `@trim` and `@drop` only rewrite values: they run first, so `@guard`,
608
+ the `@can*` family and `@validator` see the result. `@hash`, `@bind` and `@upload`
609
+ run once those have let the call through: a validator sees the password before it
610
+ is hashed, and nothing is stored or looked up for a refused call.
611
+
322
612
  ### `@convertEmptyStringsToNull`
323
613
 
324
614
  Replaces incoming empty strings `""` with `null`, before any resolver sees them —
@@ -0,0 +1,84 @@
1
+ # Generators
2
+
3
+ Lighthouse builds on graphql-ruby, and so do its generators: it writes only what
4
+ graphql-ruby has no generator for, or what an SDL-first schema needs differently.
5
+ Every graphql-ruby app's schema class and base classes (`Types::BaseObject`,
6
+ `Resolvers::BaseResolver`, `Mutations::BaseMutation`, ...) come from graphql-ruby's
7
+ own `graphql:install`, and `lighthouse:install` builds on them.
8
+
9
+ ## `lighthouse:install`
10
+
11
+ ```shell
12
+ bin/rails generate graphql:install # graphql-ruby's, with the options you choose
13
+ bin/rails generate lighthouse:install
14
+ ```
15
+
16
+ It works on what graphql-ruby's `graphql:install` wrote, with whichever of its
17
+ options the app chose (`--relay`, the default, `--batch`, `--skip-mutation-root-type`,
18
+ `--schema`, `--directory`, `--api`, ...). When the app has no graphql-ruby schema
19
+ class yet, it runs `graphql:install` first, with graphql-ruby's defaults (and
20
+ `--api` when `config.api_only = true`). Then it adapts what graphql-ruby wrote:
21
+
22
+ | File | What Lighthouse does |
23
+ | --- | --- |
24
+ | `app/graphql/app_schema.rb` | `class AppSchema < GraphQL::Schema` becomes `class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { ... } })`, naming every class graphql-ruby wrote. The rest of the class stays, but the `query(...)` / `mutation(...)` lines (the SDL's root types are built on those classes) and the `resolve_type` stub (Lighthouse resolves types). With `--batch`, `use GraphQL::Dataloader` goes beside `use GraphQL::Batch`, as Lighthouse's directives batch through the Dataloader |
25
+ | `app/graphql/types/query_type.rb`, `mutation_type.rb` | The example `test_field` goes. Relay's `node` / `nodes` stay: the SDL's `Query` is built on `Types::QueryType`, so its `field`s are the schema's too |
26
+ | `app/graphql/types/base_object.rb`, `base_interface.rb`, `base_union.rb`, `base_scalar.rb` | `include Lighthouse::GraphQL::Type`: every type class inheriting from them is Lighthouse's, `Types::QueryType` and `Types::NodeType` included |
27
+ | `app/graphql/mutations/base_mutation.rb` | Kept as graphql-ruby wrote it, a `RelayClassicMutation` (see [resolvers.md](resolvers.md#resolution-by-convention)) |
28
+ | `app/controllers/graphql_controller.rb` | Replaced by one including `Lighthouse::GraphQL::Controller` (`lighthouse_schema AppSchema`), when it is still as graphql-ruby wrote it. One the app changed is kept, and the install says how to serve the schema |
29
+
30
+ And it adds Lighthouse's own:
31
+
32
+ | File | What it is |
33
+ | --- | --- |
34
+ | `config/initializers/lighthouse.rb` | Every [configuration](configuration.md) option, commented out |
35
+ | `graphql/schema.graphql` | A starter schema, unless the folder holds `.graphql` files already |
36
+ | `graphql.config.yml` | IDE configuration, and `.gitignore` entries for the [IDE helper](getting-started.md#ide-support) files |
37
+ | `app/graphql/subscriptions/base_subscription.rb` | With `--subscriptions` only: graphql-ruby has no subscription base. It includes `Lighthouse::GraphQL::Subscription` |
38
+ | `config/routes.rb` | `post "/graphql", to: "graphql#execute"`, when missing |
39
+
40
+ | Option | Effect |
41
+ | --- | --- |
42
+ | `--schema=ApiSchema` | Which graphql-ruby schema class to adopt, and its name when `graphql:install` has to run |
43
+ | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
44
+ | `--subscriptions` | Write `Subscriptions::BaseSubscription` and name it in `base_types` |
45
+ | `--skip-controller` | Leave graphql-ruby's controller as it is, and add no route — for apps that serve GraphQL themselves |
46
+ | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
47
+
48
+ Rerunning it changes nothing: a schema class that is Lighthouse's already, a file
49
+ the app has, the route and the `.gitignore` entries are left as they are.
50
+
51
+ ## Lighthouse's generators
52
+
53
+ For the classes graphql-ruby has no generator for, or none that fits an SDL-first
54
+ schema. Each writes into the first namespace configured for its kind (or
55
+ `--namespace=…`); namespaced names nest modules (`Blog::Publish`).
56
+
57
+ | Generator | Writes |
58
+ | --- | --- |
59
+ | `lighthouse:subscription PostUpdated` | `Subscriptions::PostUpdated < BaseSubscription`, with its hooks (and the base, when missing) |
60
+ | `lighthouse:builder Published` | `Builders::Published`, for `@builder(class: "Published")` |
61
+ | `lighthouse:validator CreatePostInput` | `Validators::CreatePostInputValidator`, a `Dry::Validation::Contract` |
62
+
63
+ A subscription's arguments and return type are the SDL's, so the class declares
64
+ neither. When `Subscriptions::BaseSubscription` is missing, `lighthouse:subscription`
65
+ writes it.
66
+
67
+ A resolver or mutation class has no Lighthouse generator: write a
68
+ `Resolvers::BaseResolver` subclass by hand (see [resolvers.md](resolvers.md)), add a
69
+ method to `Types::QueryType`, or run graphql-ruby's `graphql:mutation`.
70
+
71
+ ## graphql-ruby's generators in a Lighthouse app
72
+
73
+ | Generator | In a Lighthouse app |
74
+ | --- | --- |
75
+ | `graphql:install` | The base `lighthouse:install` builds on: run it first with the options you want, or let `lighthouse:install` run it with its defaults |
76
+ | `graphql:object Post` | **Use it** for a [type class](resolvers.md#type-classes): it inherits from `Types::BaseObject`, so it is Lighthouse's; add methods. The `field` lines it writes (from the model's columns, when there is one) are ignored, as the SDL declares the fields: delete them |
77
+ | `graphql:scalar DateTime` | **Use it** for a [scalar](types.md#scalars): its `coerce_input` / `coerce_result` are the ones Lighthouse calls |
78
+ | `graphql:union SearchResult`, `graphql:interface Authored` | **Use them** when the [default resolution](types.md#the-default) is not enough: add a `resolve_type`. Their `possible_types` and `field` lines are ignored |
79
+ | `graphql:enum`, `graphql:input` | Not needed: the SDL declares enums and inputs ([`@enum`](directives.md#enum) gives a value an internal one) |
80
+ | `graphql:mutation PublishPost` | **Use it** for a code-first mutation: it writes `Mutations::PublishPost` and adds `field :publish_post, mutation: Mutations::PublishPost` to `Types::MutationType`, which the SDL's `Mutation` is built on. The class declares its own arguments and fields, as in graphql-ruby |
81
+ | `graphql:mutation_create` / `_update` / `_delete` | Code-first mutations like `graphql:mutation`'s, whose payload and input are code-first types (`Types::PostType`, `Types::PostInputType`). For a type the SDL declares, use [`@create`, `@update`, `@delete`](mutations.md) |
82
+ | `graphql:relay` | Run by `graphql:install` (`--relay` is its default). On an app installed with `--no-relay`, it adds `Types::NodeType`, `node` / `nodes` and the schema's `id_from_object` / `object_from_id`: add `node: Types::NodeType` to `base_types` by hand. See [relay.md](relay.md#relay-through-graphql-ruby) |
83
+ | `graphql:loader` | A GraphQL::Batch loader, which your own resolver classes may use; Lighthouse's directives batch through graphql-ruby's Dataloader instead |
84
+ | `graphql:detailed_trace` | **Not ready**: it looks for `class AppSchema < GraphQL::Schema`, which a Lighthouse schema class (`< Lighthouse::GraphQL::RbLightHouse.get_schema(...)`) does not match, and stops. Add `use GraphQL::Tracing::DetailedTrace` and `def self.detailed_trace?(query)` to the class body by hand, with graphql-ruby's migration (or `redis:`) |
@@ -24,32 +24,27 @@ a hard dependency of Lighthouse so non-federated apps stay lean.
24
24
  bin/rails generate lighthouse:install
25
25
  ```
26
26
 
27
- does steps 1–5 below for you:
27
+ does steps 1–5 below for you. It builds on graphql-ruby's own install,
28
+ `rails g graphql:install`, with whichever of its options the app chose (`--relay`,
29
+ `--batch`, `--api`, ...). When the app has no graphql-ruby schema class yet, it runs
30
+ `graphql:install` first, with graphql-ruby's defaults. Then it adapts what
31
+ graphql-ruby wrote, and adds Lighthouse's own:
28
32
 
29
33
  | File | What it is |
30
34
  | --- | --- |
35
+ | `app/graphql/app_schema.rb` | graphql-ruby's schema class, now built by Lighthouse from the SDL on every class graphql-ruby wrote (`base_types`). The rest of the class is graphql-ruby's |
36
+ | `app/graphql/types/query_type.rb`, `mutation_type.rb` | graphql-ruby's root types, which the SDL's `Query` and `Mutation` are built on. Their example `test_field` goes; Relay's `node` / `nodes` stay |
37
+ | `app/graphql/types/base_*.rb`, `resolvers/base_resolver.rb`, `mutations/base_mutation.rb` | graphql-ruby's base classes. `BaseObject`, `BaseInterface`, `BaseUnion` and `BaseScalar` include `Lighthouse::GraphQL::Type` |
38
+ | `app/controllers/graphql_controller.rb` | graphql-ruby's controller, replaced by one including `Lighthouse::GraphQL::Controller` |
31
39
  | `config/initializers/lighthouse.rb` | Every [configuration](configuration.md) option, commented out at its default |
32
40
  | `graphql/schema.graphql` | A starter schema (`user` via `@find`, `users` via `@paginate`) that assumes a `User` model — skipped when the folder already holds `.graphql` files |
33
- | `app/graphql/app_schema.rb` | The executable schema, with `GraphQL::Dataloader` enabled |
34
- | `app/controllers/graphql_controller.rb` | The endpoint, plus a `post '/graphql'` route |
35
41
  | `graphql.config.yml` | IDE configuration — see [IDE support](#ide-support) |
36
42
 
37
- | Option | Effect |
38
- | --- | --- |
39
- | `--schema=ApiSchema` | Name of the schema constant (default `AppSchema`) |
40
- | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
41
- | `--skip-controller` | No controller or route — for apps that already serve GraphQL |
42
- | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
43
-
44
- Rerunning it is safe: the route and `.gitignore` entries are only added once, and
45
- an existing schema class or controller is kept (pass `--force` to replace it).
46
-
47
- It coexists with graphql-ruby's own tooling. The generators live under
48
- `lighthouse:` and the rake task is `lighthouse:ide_helper`, next to graphql-ruby's
49
- `graphql:*` generators and `graphql:schema:*` tasks. `graphql:install` writes the
50
- same controller and route; whichever runs second finds the route already in place,
51
- and Lighthouse keeps a controller graphql-ruby generated. In that case, have the
52
- controller call `AppSchema.execute(...)` to serve the Lighthouse schema.
43
+ `--schema=ApiSchema`, `--sdl-folder=app/schema`, `--subscriptions`,
44
+ `--skip-controller` and `--skip-ide` change what it does. Rerunning it changes
45
+ nothing, and a controller the app changed is kept.
46
+ [generators.md](generators.md) has the details, every generator Lighthouse has, and
47
+ which of graphql-ruby's `graphql:*` generators fit a Lighthouse app.
53
48
 
54
49
  ## 1. Write your schema (SDL)
55
50
 
@@ -79,22 +74,34 @@ field's return type name (`User`), or you can be explicit with
79
74
 
80
75
  Lighthouse builds on graphql-ruby, and follows its convention: each kind of class
81
76
  inherits from a base class in its own namespace, where what they all share goes
82
- once. `rails g lighthouse:install` writes the bases of the classes that resolve
83
- root fields:
77
+ once. graphql-ruby's `graphql:install` writes them, and `lighthouse:install` builds
78
+ on them. By hand, run graphql-ruby's install with its defaults:
79
+
80
+ ```shell
81
+ bin/rails generate graphql:install
82
+ ```
83
+
84
+ Then include `Lighthouse::GraphQL::Type` in the base object, interface, union and
85
+ scalar, so every type class that inherits from them lends its methods to the SDL
86
+ type of its name (see [type classes](resolvers.md#type-classes)):
84
87
 
85
88
  ```ruby
86
- # app/graphql/resolvers/base_resolver.rb — Query fields (Resolvers::LatestPosts)
87
- module Resolvers
88
- class BaseResolver < GraphQL::Schema::Resolver
89
+ # app/graphql/types/base_object.rb
90
+ module Types
91
+ class BaseObject < GraphQL::Schema::Object
92
+ include Lighthouse::GraphQL::Type
93
+ edge_type_class(Types::BaseEdge)
94
+ connection_type_class(Types::BaseConnection)
95
+ field_class Types::BaseField
96
+ # include ApolloFederation::Object # if you use federation
89
97
  end
90
98
  end
99
+ ```
91
100
 
92
- # app/graphql/mutations/base_mutation.rb — Mutation fields (Mutations::PublishPost)
93
- module Mutations
94
- class BaseMutation < GraphQL::Schema::Mutation
95
- end
96
- end
101
+ Add the subscription base, which graphql-ruby has none of, if the schema has a
102
+ `Subscription` type (`lighthouse:install --subscriptions` writes it):
97
103
 
104
+ ```ruby
98
105
  # app/graphql/subscriptions/base_subscription.rb — Subscription fields
99
106
  module Subscriptions
100
107
  class BaseSubscription < GraphQL::Schema::Subscription
@@ -103,61 +110,85 @@ module Subscriptions
103
110
  end
104
111
  ```
105
112
 
106
- The SDL's types can have graphql-ruby base classes too, as `graphql:install`
107
- writes them: the place to mix in Apollo Federation or custom field and argument
108
- behavior.
109
-
110
- ```ruby
111
- # app/graphql/types/base_object.rb
112
- module Types
113
- class BaseObject < GraphQL::Schema::Object
114
- # include ApolloFederation::Object # if you use federation
115
- end
116
- end
117
- ```
113
+ graphql-ruby's `Mutations::BaseMutation` stays a `RelayClassicMutation`. A
114
+ `Mutation` field that declares its arguments one by one calls it as a plain
115
+ mutation, with those arguments as keywords; one with a single `input:` argument
116
+ goes through `RelayClassicMutation`'s own `input:` handling.
118
117
 
119
118
  ## 3. Build the schema
120
119
 
120
+ graphql-ruby's schema class becomes Lighthouse's: it inherits from the schema
121
+ `get_schema` builds from the SDL, instead of `GraphQL::Schema`.
122
+
121
123
  ```ruby
122
124
  # app/graphql/app_schema.rb
123
- require 'lighthouse-graphql'
124
-
125
- AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
126
- sdl_folder: Rails.root.join('graphql').to_s,
125
+ class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
127
126
  options: {
128
127
  base_types: {
128
+ query_type: Types::QueryType,
129
+ mutation_type: Types::MutationType,
130
+ node: Types::NodeType,
129
131
  resolver: Resolvers::BaseResolver,
130
132
  mutation: Mutations::BaseMutation,
131
133
  subscription: Subscriptions::BaseSubscription,
132
134
  object: Types::BaseObject,
133
- # interface:, union:, enum:, input_object:, scalar: ...
135
+ interface: Types::BaseInterface,
136
+ union: Types::BaseUnion,
137
+ scalar: Types::BaseScalar,
138
+ enum: Types::BaseEnum,
139
+ input_object: Types::BaseInputObject
134
140
  }
135
141
  }
136
142
  )
143
+ # Lighthouse batches relationship loads through graphql-ruby's Dataloader.
144
+ use GraphQL::Dataloader
145
+
146
+ max_depth(15)
147
+ max_query_string_tokens(5000)
148
+ validate_max_errors(100)
149
+
150
+ # Relay-style Object Identification, as graphql-ruby's install writes it:
151
+ def self.id_from_object(object, type_definition, query_ctx)
152
+ object.to_gid_param
153
+ end
154
+
155
+ def self.object_from_id(global_id, query_ctx)
156
+ GlobalID.find(global_id)
157
+ end
158
+ end
137
159
  ```
138
160
 
161
+ The SDL is read from `config.sdl_folder` (`get_schema(sdl_folder: ...)` overrides
162
+ it). The class body is graphql-ruby's, so plugins and settings go there as in any
163
+ graphql-ruby schema (`use ...`, `max_complexity(...)`), but for `query(...)` and
164
+ `mutation(...)`, which the SDL's root types replace, and `resolve_type`, which
165
+ Lighthouse answers (see [types.md](types.md)).
166
+
139
167
  `object:` and the other SDL kinds go to graphql-ruby, which builds the SDL's types
140
- from them. `resolver:`, `mutation:` and `subscription:` are Lighthouse's: every
141
- class a `Query`, `Mutation` or `Subscription` field resolves to must inherit from
142
- its kind's base, and a `Subscription` field with no class of its own resolves with
143
- the subscription base. See [resolvers.md](resolvers.md#base-classes).
168
+ from them. `query_type:` and `mutation_type:` are the classes the SDL's `Query` and
169
+ `Mutation` are built on, so their own `field`s (Relay's `node` / `nodes`) are the
170
+ schema's too; `node:` is the SDL's `Node` interface (see [relay.md](relay.md)).
171
+ `resolver:`, `mutation:` and `subscription:` are Lighthouse's: every class a
172
+ `Query`, `Mutation` or `Subscription` field resolves to must inherit from its kind's
173
+ base, and a `Subscription` field with no class of its own resolves with the
174
+ subscription base. See [resolvers.md](resolvers.md#base-classes).
144
175
 
145
- `get_schema` returns a `GraphQL::Schema` subclass you can use directly, or
146
- subclass further (e.g. to add federation, tracing, a Dataloader):
176
+ A constant works as well, with the settings passed as options or set on it:
147
177
 
148
178
  ```ruby
149
- class AppSchema < AppSchema # or build a Base*Schema and subclass it
150
- use GraphQL::Dataloader
151
- end
179
+ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
180
+ options: { base_types: { object: Types::BaseObject }, using: { GraphQL::Dataloader => {} } }
181
+ )
152
182
  ```
153
183
 
154
- > Lighthouse uses graphql-ruby's `Dataloader` to batch relationship loads. Add
155
- > `use GraphQL::Dataloader` to your schema to get N+1-free relationships.
184
+ > Lighthouse uses graphql-ruby's `Dataloader` to batch relationship loads. Keep
185
+ > `use GraphQL::Dataloader` in your schema to get N+1-free relationships.
156
186
 
157
187
  ## 4. Serve it
158
188
 
159
- `rails g lighthouse:install` writes the controller. It includes
160
- `Lighthouse::GraphQL::Controller`, a concern that serves the schema:
189
+ `rails g lighthouse:install` replaces graphql-ruby's controller (when the app has
190
+ not changed it) with one including `Lighthouse::GraphQL::Controller`, a concern that
191
+ serves the schema:
161
192
 
162
193
  ```ruby
163
194
  class GraphqlController < ApplicationController
@@ -290,7 +321,9 @@ When a field needs Ruby, write a graphql-ruby resolver or mutation class named
290
321
  after it — `Query.latestPosts` resolves to `Resolvers::LatestPosts`
291
322
  (`app/graphql/resolvers/latest_posts.rb`, a `Resolvers::BaseResolver`),
292
323
  `Mutation.publishPost` to `Mutations::PublishPost` (a `Mutations::BaseMutation`).
293
- `bin/rails generate lighthouse:query LatestPosts` scaffolds one. See
324
+ A method of the root type class comes first, as in graphql-ruby: `Query.latestPosts`
325
+ calls `Types::QueryType#latest_posts` when it defines one. `bin/rails generate
326
+ graphql:mutation PublishPost` writes a code-first mutation. See
294
327
  [resolvers.md](resolvers.md).
295
328
 
296
329
  ## Next steps