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/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
@@ -150,8 +173,9 @@ The relation name defaults to the field name (snake_cased). Override with
150
173
 
151
174
  Delegate a field to a class — your escape hatch for anything the built-in
152
175
  directives don't cover. On `Query` and `Mutation` you usually don't need it:
153
- a field without a resolving directive resolves to `Queries::<FieldName>` /
154
- `Mutations::<FieldName>`, graphql-ruby resolver and mutation classes. See
176
+ a field without a resolving directive resolves to `Resolvers::<FieldName>` /
177
+ `Mutations::<FieldName>`, graphql-ruby resolver and mutation classes under
178
+ `Resolvers::BaseResolver` / `Mutations::BaseMutation`. See
155
179
  [resolvers.md](resolvers.md).
156
180
 
157
181
  ```graphql
@@ -161,10 +185,16 @@ type Contact {
161
185
  ```
162
186
 
163
187
  On `Query` and `Mutation` the name is looked up in the queries / mutations
164
- 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
165
194
  be a `GraphQL::Schema::Resolver` (graphql-ruby's contract), or a plain callable —
166
195
  a class-level `call`, an instance `#call`, or an instance `#resolve`, each
167
- receiving `(object, args, context)`:
196
+ receiving `(object, args, context)`, and the [`ResolveInfo`](resolvers.md#resolveinfo)
197
+ as a fourth argument when it takes one:
168
198
 
169
199
  ```ruby
170
200
  module Resolvers
@@ -176,6 +206,17 @@ module Resolvers
176
206
  end
177
207
  ```
178
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
+
179
220
  ### `@rename`
180
221
 
181
222
  Read a differently-named attribute off the parent object. This is the explicit
@@ -190,15 +231,157 @@ type User {
190
231
  ### `@method`
191
232
 
192
233
  Resolve a field by calling a method on the parent object (PHP-parity). Great for
193
- 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.
194
236
 
195
237
  ```graphql
196
238
  type Conversation {
197
239
  labels: [String!]! @method(name: "label_list")
198
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
199
281
  }
200
282
  ```
201
283
 
284
+ ### `@throttle`
285
+
286
+ Rate-limits a field per client: at most `maxAttempts` calls every `decayMinutes`
287
+ (default 60 a minute), counted in [`cache_store`](configuration.md#cache_store). A
288
+ client is the signed-in user, else the context's `:remote_ip`, which the controller
289
+ concern sets. Past the limit the field fails with `Rate limit for Query.x exceeded.
290
+ Try again later.` before anything else runs. `name:` makes fields count together,
291
+ `prefix:` separates groups.
292
+
293
+ ```graphql
294
+ type Mutation {
295
+ login(email: String!, password: String!): User @throttle(maxAttempts: 5)
296
+ }
297
+ ```
298
+
299
+ ### `@async`
300
+
301
+ Runs a mutation in a job and answers `true` at once. `@guard`, the `@can*` family,
302
+ `@validator` and the argument directives run in the request, so the client still
303
+ gets their errors; what resolves the field runs in
304
+ `Lighthouse::GraphQL::AsyncMutationJob` (ActiveJob), with the arguments and the
305
+ context values ActiveJob can serialize (records too, through GlobalID). The field
306
+ must be a `Mutation` field returning `Boolean`, and the schema a named constant.
307
+
308
+ ```graphql
309
+ type Mutation {
310
+ importFeed(url: String!): Boolean! @guard @async(queue: "imports")
311
+ }
312
+ ```
313
+
314
+ ### `@hide` and `@show`
315
+
316
+ Leave a field out of the schema by environment (`Rails.env`): `@hide(env:)` removes
317
+ it in the environments listed, `@show(env:)` keeps it only in those.
318
+
319
+ ```graphql
320
+ type Query {
321
+ debugInfo: String @show(env: ["development", "test"])
322
+ legacyFeed: [Post!]! @hide(env: ["production"]) @all
323
+ }
324
+ ```
325
+
326
+ ## Type directives
327
+
328
+ ### `@model`
329
+
330
+ Maps an object type to the ActiveRecord model whose records it resolves, when the
331
+ names differ, as PHP Lighthouse's. Every directive that takes `model:` (`@all`,
332
+ `@find`, `@first`, `@paginate`, `@create`, `@update`, `@upsert`, `@delete`, the
333
+ `@can*` family...) on a field returning the type gets it, unless it names its own; a
334
+ union or interface resolves the model's records to the type; `@node` finds them. The
335
+ schema does not build unless the class is an ActiveRecord model.
336
+
337
+ ```graphql
338
+ type Article @model(class: "Post") { id: ID! title: String! }
339
+
340
+ type Query {
341
+ articles: [Article!]! @paginate # pages through Post
342
+ article(id: ID! @eq): Article @find
343
+ }
344
+ ```
345
+
346
+ ### `@enum`
347
+
348
+ The internal value of an enum value: what a resolver gets for it in an argument,
349
+ and what a result answers with it for.
350
+
351
+ ```graphql
352
+ enum Role {
353
+ ADMIN @enum(value: "admin")
354
+ MEMBER @enum(value: "member")
355
+ }
356
+
357
+ type Query { users(role: Role @eq): [User!]! @all } # filters on "admin"
358
+ ```
359
+
360
+ They go on a union, an interface or a scalar, and name the class that resolves or
361
+ coerces its values. Without them, Lighthouse uses the type's class
362
+ (`Types::<Name>Type` including `Lighthouse::GraphQL::Type`), or its default. See
363
+ [types.md](types.md).
364
+
365
+ ### `@union` and `@interface`
366
+
367
+ Name the class that picks the object type of each value: `resolve_type(object,
368
+ context)` or `call(object, context)`, returning a type name or a type class.
369
+ Looked up in the types namespace (nested names too), then as written.
370
+
371
+ ```graphql
372
+ union SearchResult @union(resolveType: "SearchResultResolver") = Post | User
373
+ interface Authored @interface(resolveType: "AuthoredResolver") { id: ID! }
374
+ ```
375
+
376
+ ### `@scalar`
377
+
378
+ Names the class that reads and writes the scalar's values: `coerce_input(value,
379
+ context)` and `coerce_result(value, context)`, as a `GraphQL::Schema::Scalar`.
380
+
381
+ ```graphql
382
+ scalar DateTime @scalar(class: "Types::DateTimeType")
383
+ ```
384
+
202
385
  ---
203
386
 
204
387
  ## Authentication & Authorization
@@ -262,7 +445,40 @@ type Query {
262
445
  ```
263
446
 
264
447
  Builders are looked up in their own namespace, `Builders::` (app/graphql/builders)
265
- — they are not resolvers. See [resolvers.md](resolvers.md#builders).
448
+ — they are not resolvers. See [resolvers.md](resolvers.md#builders). The class is
449
+ `class:` with `method:` (default `call`), or PHP Lighthouse's `method: "Class@method"`.
450
+
451
+ On a field, the method gets `(relation, args, context)`, or the directive's
452
+ `value:` in place of the arguments. On an argument or an input field, it gets
453
+ `(relation, value, context)` whenever the client sends that argument, an explicit
454
+ `null` included, and not when it is left out:
455
+
456
+ ```graphql
457
+ type Query {
458
+ posts(since: DateTime @builder(method: "Recent@since")): [Post!]! @all
459
+ latest: [Post!]! @all @builder(class: "Recent", value: 5)
460
+ }
461
+ ```
462
+
463
+ ```ruby
464
+ class Builders::Recent
465
+ def self.call(relation, count, _context) = relation.order(created_at: :desc).limit(count)
466
+ def self.since(relation, time, _context) = time ? relation.where(created_at: time..) : relation
467
+ end
468
+ ```
469
+
470
+ ### `@whereAuth`
471
+
472
+ Keep only the records the signed-in user owns, through a relation of the model: a
473
+ `belongs_to`, a `has_many`, a `:through`. A guest gets none. It applies where
474
+ `@builder` does (`@all`, `@paginate`, `@first`, `@count`, `enhance_builder`).
475
+
476
+ ```graphql
477
+ type Query {
478
+ myPosts: [Post!]! @paginate @whereAuth(relation: "author")
479
+ myTeams: [Team!]! @all @whereAuth(relation: "members")
480
+ }
481
+ ```
266
482
 
267
483
  ```ruby
268
484
  module Builders
@@ -291,6 +507,10 @@ Full guide: [filtering-and-ordering.md](filtering-and-ordering.md).
291
507
  | `@like` | `column LIKE %value%` |
292
508
  | `@whereKey` | `primary key = value` (`IN` for a list) |
293
509
  | `@where(operator:)` | generic operator (`GT`, `GTE`, `IN`, `IS_NULL`, `CONTAINS`, …) |
510
+ | `@whereNull` / `@whereNotNull` | `true`: `column IS NULL` / `IS NOT NULL`; `false`: the opposite |
511
+ | `@whereBetween` / `@whereNotBetween` | `column BETWEEN` / `NOT BETWEEN` the input's two fields, in order |
512
+ | `@whereJsonContains` | a JSON column contains the value (every value of a list); `key: "meta->tags"` points into it |
513
+ | `@scope(name:)` | calls the model scope with the value (`name:` defaults to the argument's) |
294
514
  | `@orderBy` | sort by one or more columns |
295
515
 
296
516
  ```graphql
@@ -306,6 +526,41 @@ type Query {
306
526
 
307
527
  The column defaults to the snake_cased argument name; override with `key:`.
308
528
 
529
+ ```graphql
530
+ type Query {
531
+ posts(
532
+ unpublished: Boolean @whereNull(key: "published_at")
533
+ createdAt: DateRange @whereBetween(key: "created_at")
534
+ tagged: [String!] @whereJsonContains(key: "tags")
535
+ trending: Boolean @scope
536
+ filter: PostFilter @nest
537
+ limit: Int @limit
538
+ ): [Post!]! @all
539
+ }
540
+
541
+ input DateRange { from: DateTime! to: DateTime! }
542
+ input PostFilter { title: String @like, authorId: ID @eq(key: "author_id") }
543
+ ```
544
+
545
+ ### `@nest`
546
+
547
+ On an argument whose type is an input object, applies the filters of the input's
548
+ fields (at any depth, for those that carry `@nest` too), as if they were the
549
+ field's own arguments.
550
+
551
+ ### `@limit`
552
+
553
+ On an `Int` argument, caps how many results the field returns at the client's
554
+ value; `null` leaves them alone. A query not run yet gets a `LIMIT`; the records of
555
+ a relation, loaded for every parent at once, are cut per parent. A negative value is
556
+ an error.
557
+
558
+ `@limit(builder: true)` puts the `LIMIT` on the field's query instead, with the
559
+ other argument directives. Use it on root fields: on a relation batched for many
560
+ parents it would cut them all together, as PHP Lighthouse warns. PHP Lighthouse
561
+ also allows `@limit` on the field itself, where it only marks the field the argument
562
+ limits; it is accepted there and changes nothing.
563
+
309
564
  ### `@whereConditions` / `@whereHasConditions`
310
565
 
311
566
  Give clients a structured, dynamic `WHERE` builder (AND/OR/operators, and HAS for
@@ -318,10 +573,39 @@ type Query {
318
573
  }
319
574
  ```
320
575
 
576
+ ### `@trim`, `@hash`, `@drop`, `@bind` and `@upload`
577
+
578
+ Rewrite an argument's or input field's value before the field resolves, in the
579
+ order the SDL lists them:
580
+
581
+ | Directive | The resolver gets |
582
+ | --- | --- |
583
+ | `@trim` | the string without leading and trailing whitespace. On a field: every string it takes, nested inputs included |
584
+ | `@drop` | nothing: the argument is removed |
585
+ | `@hash` | the value hashed with BCrypt (the `bcrypt` gem, as `has_secure_password`) |
586
+ | `@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 |
587
+ | `@upload(disk:, path: "/")` | what [`upload_store`](configuration.md#upload_store) answers for the uploaded file: the signed id of an ActiveStorage blob, by default ([uploads.md](uploads.md)) |
588
+
589
+ ```graphql
590
+ type Mutation {
591
+ signUp(email: String! @trim, password: String! @hash, captcha: String @drop): User! @create
592
+ assign(task: ID! @bind(class: "Task"), user: ID! @bind(class: "User", with: ["roles"])): Task!
593
+ updateAvatar(avatar: Upload! @upload(path: "avatars")): User! @update @inject(context: "current_user.id", name: "id")
594
+ }
595
+ ```
596
+
597
+ `@bind` fails the field with a validation error on the argument (`The selected task
598
+ is invalid.`) when no record matches, unless `optional: true`, which passes `null`.
599
+
600
+ `@globalId`, `@trim` and `@drop` only rewrite values: they run first, so `@guard`,
601
+ the `@can*` family and `@validator` see the result. `@hash`, `@bind` and `@upload`
602
+ run once those have let the call through: a validator sees the password before it
603
+ is hashed, and nothing is stored or looked up for a refused call.
604
+
321
605
  ### `@convertEmptyStringsToNull`
322
606
 
323
607
  Replaces incoming empty strings `""` with `null`, before any resolver sees them —
324
- directive resolvers (`@create`, `@all` filters, …), `@field`, `Queries::` /
608
+ directive resolvers (`@create`, `@all` filters, …), `@field`, `Resolvers::` /
325
609
  `Mutations::` classes and `@validator` alike. On an argument or an input field it
326
610
  converts that one input; on a field it converts every input of the field, nested
327
611
  input objects and lists included. Non-null inputs (`String!`) pass unchanged when
@@ -520,9 +804,9 @@ directive @subscription(class: String!) on FIELD_DEFINITION
520
804
 
521
805
  Names the class a `Subscription` field resolves to, when it does not follow the
522
806
  convention (`postUpdated` → `Subscriptions::PostUpdated`). The class inherits from
523
- `GraphQL::Schema::Subscription` (which may `include Lighthouse::GraphQL::Subscription`,
524
- the optional concern with PHP Lighthouse's `filter?` hooks) and is looked up in
525
- `Config.namespaces[:subscriptions]`; a name with `::` is used as it is.
807
+ `Subscriptions::BaseSubscription`, a `GraphQL::Schema::Subscription` that includes
808
+ `Lighthouse::GraphQL::Subscription` (PHP Lighthouse's `filter?` hooks), and is looked
809
+ up in `Config.namespaces[:subscriptions]`; a name with `::` is used as it is.
526
810
 
527
811
  ```graphql
528
812
  type Subscription {
@@ -634,6 +918,30 @@ Flipper.enable_actor(:new_checkout, User.find(1))
634
918
 
635
919
  ---
636
920
 
921
+ ## Query complexity
922
+
923
+ ### `@complexity`
924
+
925
+ Customize what a field costs in the query's complexity score, which graphql-ruby
926
+ checks against the schema's `max_complexity` before it runs the query.
927
+
928
+ ```graphql
929
+ type Query {
930
+ search(first: Int, term: String!): [Post!]! @complexity
931
+ posts(includeFullText: Boolean): [Post!]! @all @complexity(resolver: "PostsComplexity")
932
+ }
933
+ ```
934
+
935
+ | Argument | Meaning |
936
+ | --- | --- |
937
+ | `resolver: String` | A `GraphQL::Analysis::Analyzer` subclass in `namespaces[:analyzers]` (`Analyzers::PostsComplexity`), installed on the schema. With `Lighthouse::GraphQL::ComplexityAnalyzer` included, its `complexity` (a number, or a proc of `context, args, child_complexity`) is the field's cost. Without it, or without `resolver:`: `1 + child complexity × first`. |
938
+
939
+ Paginated fields cost `1 + child complexity × first` without the directive, and a
940
+ field resolved by a `GraphQL::Schema::Resolver` class costs what that class
941
+ declares. See [security.md](security.md).
942
+
943
+ ---
944
+
637
945
  ## Events
638
946
 
639
947
  `@event` dispatches an event once the field has resolved. The class is built with
data/docs/events.md CHANGED
@@ -107,7 +107,9 @@ end
107
107
  # => { "data" => {...}, "extensions" => { "queries" => 3 } }
108
108
  ```
109
109
 
110
- `query_complexity` is `nil`: Lighthouse does not compute it for you.
110
+ `query_complexity` is the query's complexity score, the number graphql-ruby
111
+ checks against `max_complexity` (see [security.md](security.md#reading-the-score)).
112
+ It is `nil` for a query that failed validation.
111
113
 
112
114
  **`ManipulateResult`**: change `event.result` (the Hash with `data`, `errors` and
113
115
  `extensions`) in place, or assign a new Hash to `event.result=`:
data/docs/federation.md CHANGED
@@ -13,7 +13,8 @@ Include the federation mixins in the base types you pass to Lighthouse, and make
13
13
  your schema a federation schema:
14
14
 
15
15
  ```ruby
16
- class BaseObject < GraphQL::Schema::Object
16
+ # app/graphql/types/base_object.rb
17
+ class Types::BaseObject < GraphQL::Schema::Object
17
18
  include ApolloFederation::Object
18
19
 
19
20
  # Default entity resolution: look the record up by id on the model that shares
@@ -29,7 +30,7 @@ end
29
30
 
30
31
  BaseAppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
31
32
  sdl_folder: Rails.root.join('graphql').to_s,
32
- options: { federation: true, base_types: { object: BaseObject } }
33
+ options: { federation: true, base_types: { object: Types::BaseObject, resolver: Resolvers::BaseResolver } }
33
34
  )
34
35
 
35
36
  class AppSchema < BaseAppSchema
@@ -0,0 +1,82 @@
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 base classes (`Types::BaseObject`, `Resolvers::BaseResolver`,
6
+ `Mutations::BaseMutation`, ...) come from graphql-ruby's own `graphql:install`.
7
+
8
+ ## `lighthouse:install`
9
+
10
+ ```shell
11
+ bin/rails generate lighthouse:install
12
+ ```
13
+
14
+ It runs graphql-ruby's install first
15
+ (`graphql:install --no-relay --skip-graphiql --skip-query-logs`), which writes:
16
+
17
+ - `app/graphql/types/base_*.rb`: `BaseObject`, `BaseField`, `BaseArgument`,
18
+ `BaseInterface`, `BaseUnion`, `BaseScalar`, `BaseEnum`, `BaseInputObject`;
19
+ - `app/graphql/resolvers/base_resolver.rb` and `app/graphql/mutations/base_mutation.rb`;
20
+ - its schema class, `Types::QueryType`, `Types::MutationType` and controller, and the
21
+ `POST /graphql` route.
22
+
23
+ Then Lighthouse puts its own on top of what graphql-ruby wrote in that run:
24
+
25
+ | File | What Lighthouse does |
26
+ | --- | --- |
27
+ | `app/graphql/app_schema.rb` | Replaced by the schema built from the SDL, naming every base class in `base_types` |
28
+ | `app/controllers/graphql_controller.rb` | Replaced by one including `Lighthouse::GraphQL::Controller` |
29
+ | `app/graphql/types/query_type.rb`, `mutation_type.rb` | Removed: the SDL declares the root types |
30
+ | `app/graphql/mutations/base_mutation.rb` | Made a `GraphQL::Schema::Mutation`: the SDL declares a mutation's arguments, where graphql-ruby's `RelayClassicMutation` takes a single `input:` |
31
+ | `app/graphql/subscriptions/base_subscription.rb` | Written: graphql-ruby has no subscription base. It includes `Lighthouse::GraphQL::Subscription` |
32
+ | `config/initializers/lighthouse.rb` | Written: every [configuration](configuration.md) option, commented out |
33
+ | `graphql/schema.graphql` | A starter schema, unless the folder holds `.graphql` files already |
34
+ | `graphql.config.yml` | IDE configuration, and `.gitignore` entries for the [IDE helper](getting-started.md#ide-support) files |
35
+
36
+ | Option | Effect |
37
+ | --- | --- |
38
+ | `--schema=ApiSchema` | Name of the schema constant (default `AppSchema`) |
39
+ | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
40
+ | `--skip-controller` | No controller or route (graphql-ruby's are removed too) — for apps that serve GraphQL themselves |
41
+ | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
42
+ | `--force` | Replace a schema class or controller the app had before |
43
+
44
+ Rerunning it is safe: a file the app has is kept, and the route and `.gitignore`
45
+ entries are added once. In an app that already had a graphql-ruby schema class or
46
+ controller, those are kept (Lighthouse replaces only what graphql-ruby wrote in the
47
+ same run), and graphql-ruby's install points that schema at the root types it
48
+ wrote: have the controller call `AppSchema.execute(...)` to serve Lighthouse's
49
+ schema, or pass `--force`.
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:query LatestPosts` | `Resolvers::LatestPosts < BaseResolver`, which `Query.latestPosts` resolves to by convention |
60
+ | `lighthouse:mutation PublishPost` | `Mutations::PublishPost < BaseMutation`, for `Mutation.publishPost` |
61
+ | `lighthouse:subscription PostUpdated` | `Subscriptions::PostUpdated < BaseSubscription`, with its hooks (and the base, when missing) |
62
+ | `lighthouse:builder Published` | `Builders::Published`, for `@builder(class: "Published")` |
63
+ | `lighthouse:validator CreatePostInput` | `Validators::CreatePostInputValidator`, a `Dry::Validation::Contract` |
64
+
65
+ The class's arguments and return type are the SDL's, so it declares neither. When
66
+ the base class is missing (an app that never ran an install, or a namespace of its
67
+ own such as `Api::Resolvers`), the generator says so: `lighthouse:install` writes it
68
+ through graphql-ruby, or write `Api::Resolvers::BaseResolver` yourself.
69
+
70
+ ## graphql-ruby's generators in a Lighthouse app
71
+
72
+ | Generator | In a Lighthouse app |
73
+ | --- | --- |
74
+ | `graphql:install` | Run by `lighthouse:install`. On its own, it writes a code-first schema, root types and a `RelayClassicMutation` base: run `lighthouse:install` instead |
75
+ | `graphql:object Post` | **Use it** for a [type class](resolvers.md#type-classes): add `include Lighthouse::GraphQL::Type`, then 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 |
76
+ | `graphql:scalar DateTime` | **Use it** for a [scalar](types.md#scalars): add `include Lighthouse::GraphQL::Type`; its `coerce_input` / `coerce_result` are the ones Lighthouse calls |
77
+ | `graphql:union SearchResult`, `graphql:interface Authored` | **Use them** when the [default resolution](types.md#the-default) is not enough: add `include Lighthouse::GraphQL::Type` and a `resolve_type`. Its `possible_types` and `field` lines are ignored |
78
+ | `graphql:enum`, `graphql:input` | Not needed: the SDL declares enums and inputs ([`@enum`](directives.md#enum) gives a value an internal one) |
79
+ | `graphql:mutation`, `graphql:mutation_create` / `_update` / `_delete` | **Not ready**: they add a field to `Types::MutationType`, which a Lighthouse app has none of (they write it again), and write a `RelayClassicMutation` taking `input:`. Use `lighthouse:mutation`, or [`@create`, `@update`, `@delete`](mutations.md) |
80
+ | `graphql:relay` | Not needed: [`@node`, `@globalId`](relay.md) and `@paginate(type: CONNECTION)` |
81
+ | `graphql:loader` | A GraphQL::Batch loader, which your own resolver classes may use; Lighthouse's directives batch through graphql-ruby's Dataloader instead |
82
+ | `graphql:detailed_trace` | **Not ready**: it edits a `class AppSchema < GraphQL::Schema`, which Lighthouse's `AppSchema = get_schema(...)` is not. Add its migration and `AppSchema.use(GraphQL::Tracing::DetailedTrace)` by hand |