lighthouse-graphql 0.5.0 → 0.7.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -1
  3. data/README.md +38 -22
  4. data/docs/best-practices.md +6 -4
  5. data/docs/caching.md +4 -3
  6. data/docs/configuration.md +20 -26
  7. data/docs/custom-directives.md +2 -1
  8. data/docs/directives.md +79 -14
  9. data/docs/federation.md +1 -1
  10. data/docs/filtering-and-ordering.md +330 -30
  11. data/docs/generators.md +42 -40
  12. data/docs/getting-started.md +128 -58
  13. data/docs/motivation.md +3 -2
  14. data/docs/performance.md +5 -4
  15. data/docs/relationships.md +7 -5
  16. data/docs/relay.md +63 -0
  17. data/docs/resolvers.md +92 -39
  18. data/docs/schema-organisation.md +177 -0
  19. data/docs/security.md +64 -16
  20. data/docs/subscriptions.md +16 -9
  21. data/docs/types.md +16 -17
  22. data/docs/uploads.md +41 -14
  23. data/lib/generators/lighthouse/base_classes.rb +9 -14
  24. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  25. data/lib/generators/lighthouse/install/install_generator.rb +125 -99
  26. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  27. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
  28. data/lib/lighthouse/ast_builder.rb +124 -0
  29. data/lib/lighthouse/caching.rb +9 -3
  30. data/lib/lighthouse/config.rb +27 -25
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  32. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  33. data/lib/lighthouse/controller.rb +28 -5
  34. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  36. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
  37. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  38. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  39. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  40. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  41. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  42. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  43. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
  44. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  45. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
  46. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
  47. data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
  48. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  49. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  50. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  51. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  52. data/lib/lighthouse/directives/definitions.rb +0 -1
  53. data/lib/lighthouse/directives/generated_types.rb +103 -0
  54. data/lib/lighthouse/directives/registry.rb +39 -10
  55. data/lib/lighthouse/document_ast.rb +111 -0
  56. data/lib/lighthouse/field_resolver_stack.rb +9 -6
  57. data/lib/lighthouse/global_id.rb +29 -3
  58. data/lib/lighthouse/graphql/version.rb +1 -1
  59. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  60. data/lib/lighthouse/ide_helper.rb +80 -23
  61. data/lib/lighthouse/namespaces.rb +1 -0
  62. data/lib/lighthouse/order_by/types.rb +137 -0
  63. data/lib/lighthouse/railtie.rb +9 -2
  64. data/lib/lighthouse/rb_lighthouse.rb +16 -5
  65. data/lib/lighthouse/resolve_info.rb +5 -5
  66. data/lib/lighthouse/resolver_bases.rb +18 -4
  67. data/lib/lighthouse/resolver_dispatch.rb +16 -1
  68. data/lib/lighthouse/root_types.rb +117 -0
  69. data/lib/lighthouse/schema_cache.rb +8 -4
  70. data/lib/lighthouse/schema_factory.rb +6 -9
  71. data/lib/lighthouse/schema_generator.rb +8 -18
  72. data/lib/lighthouse/schema_stitcher.rb +64 -0
  73. data/lib/lighthouse/support/naming.rb +0 -12
  74. data/lib/lighthouse/upload.rb +4 -2
  75. data/lib/lighthouse/upload_store.rb +39 -14
  76. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  77. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  78. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  79. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  80. data/lib/lighthouse/where_conditions/types.rb +110 -0
  81. data/sig/lighthouse/directives.rbs +16 -3
  82. data/sig/lighthouse/graphql.rbs +48 -4
  83. data/sig/lighthouse.rbs +8 -12
  84. metadata +17 -7
  85. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
  86. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  87. data/lib/generators/lighthouse/query_generator.rb +0 -30
  88. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  89. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  90. data/lib/lighthouse/sdl_loader.rb +0 -22
data/docs/resolvers.md CHANGED
@@ -8,9 +8,9 @@ interchangeable. Each kind has its own contract and its own folder:
8
8
  | Kind | Folder / namespace | Contract | Used by |
9
9
  | --- | --- | --- | --- |
10
10
  | Resolver | `app/graphql/resolvers` · `Resolvers::` | `Resolvers::BaseResolver` (a `GraphQL::Schema::Resolver`) | `Query` fields |
11
- | Mutation | `app/graphql/mutations` · `Mutations::` | `Mutations::BaseMutation` (a `GraphQL::Schema::Mutation`) | `Mutation` fields |
11
+ | Mutation | `app/graphql/mutations` · `Mutations::` | `Mutations::BaseMutation` (graphql-ruby's `GraphQL::Schema::RelayClassicMutation`) | `Mutation` fields |
12
12
  | Subscription | `app/graphql/subscriptions` · `Subscriptions::` | `Subscriptions::BaseSubscription` (a `GraphQL::Schema::Subscription`) | `Subscription` fields ([subscriptions.md](subscriptions.md)) |
13
- | Type | `app/graphql/types` · `Types::` | `Types::BaseObject` (a `GraphQL::Schema::Object`) including `Lighthouse::GraphQL::Type`, named `<Type>Type` | Fields of other types, through its methods ([type classes](#type-classes)); unions, interfaces and scalars too ([types.md](types.md)) |
13
+ | Type | `app/graphql/types` · `Types::` | `Types::BaseObject` (a `GraphQL::Schema::Object`) including `Lighthouse::GraphQL::Type`, named `<Type>Type` | Fields of any object type, through its methods ([type classes](#type-classes), [root type classes](#root-type-classes)); unions, interfaces and scalars too ([types.md](types.md)) |
14
14
  | Builder | `app/graphql/builders` · `Builders::` | `call(relation, args, context)` → relation | `@builder` |
15
15
 
16
16
  Validators (`app/graphql/validators`) are one more kind. They are covered in
@@ -19,12 +19,13 @@ Validators (`app/graphql/validators`) are one more kind. They are covered in
19
19
  ## Base classes
20
20
 
21
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
+ and every class of the kind inherits from it. graphql-ruby's `graphql:install`
23
+ writes `Resolvers::BaseResolver`, `Mutations::BaseMutation` and the `Types::Base*`
24
+ classes, and `rails g lighthouse:install` builds on them: it keeps them as
25
+ graphql-ruby wrote them, includes `Lighthouse::GraphQL::Type` in the base object,
26
+ interface, union and scalar, and with `--subscriptions` adds
27
+ `Subscriptions::BaseSubscription`, which graphql-ruby has none of
28
+ ([generators.md](generators.md)):
28
29
 
29
30
  ```ruby
30
31
  # app/graphql/resolvers/base_resolver.rb (graphql-ruby's)
@@ -33,16 +34,17 @@ module Resolvers
33
34
  end
34
35
  end
35
36
 
36
- # app/graphql/mutations/base_mutation.rb (graphql-ruby's, made a Mutation)
37
+ # app/graphql/mutations/base_mutation.rb (graphql-ruby's)
37
38
  module Mutations
38
- class BaseMutation < GraphQL::Schema::Mutation
39
+ class BaseMutation < GraphQL::Schema::RelayClassicMutation
39
40
  argument_class Types::BaseArgument
40
41
  field_class Types::BaseField
42
+ input_object_class Types::BaseInputObject
41
43
  object_class Types::BaseObject
42
44
  end
43
45
  end
44
46
 
45
- # app/graphql/subscriptions/base_subscription.rb (Lighthouse's)
47
+ # app/graphql/subscriptions/base_subscription.rb (Lighthouse's, --subscriptions)
46
48
  module Subscriptions
47
49
  class BaseSubscription < GraphQL::Schema::Subscription
48
50
  include Lighthouse::GraphQL::Subscription
@@ -62,9 +64,12 @@ schema holds every class to them:
62
64
 
63
65
  ```ruby
64
66
  # app/graphql/app_schema.rb
65
- AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
67
+ class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
66
68
  options: {
67
69
  base_types: {
70
+ query_type: Types::QueryType,
71
+ mutation_type: Types::MutationType,
72
+ node: Types::NodeType,
68
73
  resolver: Resolvers::BaseResolver,
69
74
  mutation: Mutations::BaseMutation,
70
75
  subscription: Subscriptions::BaseSubscription,
@@ -77,6 +82,8 @@ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
77
82
  }
78
83
  }
79
84
  )
85
+ use GraphQL::Dataloader
86
+ end
80
87
  ```
81
88
 
82
89
  - A class a `Query`, `Mutation` or `Subscription` field resolves to, by convention
@@ -89,12 +96,17 @@ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
89
96
  (`GraphQL::Schema::Resolver`, `GraphQL::Schema::Mutation`, which
90
97
  `RelayClassicMutation` is, `GraphQL::Schema::Subscription`), or the schema does not
91
98
  build. graphql-ruby never sees these three keys.
99
+ - `query_type:` and `mutation_type:` are the code-first classes the SDL's `Query`
100
+ and `Mutation` are built on (see [root type classes](#root-type-classes)), and
101
+ `node:` is the SDL's `Node` interface ([relay.md](relay.md#relay-through-graphql-ruby)).
102
+ `subscription_type:` does the same for `Subscription`.
92
103
  - The others are graphql-ruby's: it builds the SDL's types of each kind from them.
93
104
  A [type class](#type-classes) (one including `Lighthouse::GraphQL::Type`) that
94
105
  does not inherit from its kind's raises `Lighthouse::DefinitionError` when the
95
106
  schema is built.
96
107
 
97
- `rails g lighthouse:install` writes the schema class with them. Without them,
108
+ `rails g lighthouse:install` names in the schema class every one graphql-ruby's
109
+ install wrote. Without them,
98
110
  Lighthouse requires only the graphql-ruby class (`GraphQL::Schema::Resolver`, …),
99
111
  and a `Subscription` field with no class resolves with a built-in subscription that
100
112
  includes the concern.
@@ -149,8 +161,9 @@ end
149
161
 
150
162
  ### Resolution by convention
151
163
 
152
- A field on `Query` or `Mutation` with **no resolving directive** resolves to the
153
- class named after it, as in PHP Lighthouse:
164
+ A field on `Query` or `Mutation` with **no resolving directive**, and no method of
165
+ its name on the [root type class](#root-type-classes), resolves to the class named
166
+ after it, as in PHP Lighthouse:
154
167
 
155
168
  - `Query.latestPosts` resolves to `Resolvers::LatestPosts`;
156
169
  - `Mutation.publishPost` resolves to `Mutations::PublishPost`.
@@ -162,9 +175,11 @@ field with no class is reported through [`on_error`](configuration.md#on_error):
162
175
  it raises in test and logs elsewhere. Lighthouse never falls back to a top-level
163
176
  constant, so a `post` field is never mistaken for the `Post` model.
164
177
 
165
- graphql-ruby's `RelayClassicMutation` (what `rails g graphql:install` makes
166
- `Mutations::BaseMutation` inherit from) works too, as long as the SDL field takes the
167
- `input:` argument it expects.
178
+ graphql-ruby's `RelayClassicMutation`, which `Mutations::BaseMutation` inherits from,
179
+ takes a single `input:` argument. A `Mutation` field whose SDL declares its arguments
180
+ one by one calls the class as a plain mutation, with those arguments as keywords
181
+ (`resolve(id:)` above). A field with a single `input:` argument goes through
182
+ `RelayClassicMutation`'s own `input:` handling.
168
183
 
169
184
  Wrapping directives (`@guard`, the `@can*` family, `@validator`, `@cache`,
170
185
  `@clearCache`) are not resolvers, so a field carrying only those still resolves by
@@ -176,6 +191,45 @@ type Mutation {
176
191
  }
177
192
  ```
178
193
 
194
+ ### Root type classes
195
+
196
+ graphql-ruby's install writes `Types::QueryType` and `Types::MutationType`, and the
197
+ schema's `base_types` name them (`query_type:`, `mutation_type:`). The SDL's `Query`
198
+ and `Mutation` are built on them, so they work as in a graphql-ruby app:
199
+
200
+ - A root field with no resolving directive calls the method of its snake_case name
201
+ on the class, when the class defines it and includes `Lighthouse::GraphQL::Type`
202
+ (`Types::BaseObject` does, after `lighthouse:install`). It comes before the
203
+ conventional class: `Query.latestPosts` calls `Types::QueryType#latest_posts`
204
+ before `Resolvers::LatestPosts`.
205
+ - The class's own `field`s are the schema's too, beside the SDL's, and resolve as
206
+ in graphql-ruby: Relay's `node` / `nodes`, and the `field :publish_post, mutation:
207
+ Mutations::PublishPost` that `rails g graphql:mutation` adds. An SDL field of the
208
+ same name wins.
209
+ - When the SDL has no `Mutation` type, `Types::MutationType` itself is the root,
210
+ once it declares a field.
211
+
212
+ ```graphql
213
+ type Query {
214
+ latestPosts(limit: Int = 10): [Post!]!
215
+ }
216
+ ```
217
+
218
+ ```ruby
219
+ # app/graphql/types/query_type.rb
220
+ module Types
221
+ class QueryType < Types::BaseObject
222
+ def latest_posts(limit:)
223
+ Post.order(created_at: :desc).limit(limit)
224
+ end
225
+ end
226
+ end
227
+ ```
228
+
229
+ As in a [type class](#type-classes), the method has `object`, `context`, the
230
+ `dataload*` helpers and `resolve_info`, and gets the field's arguments as snake_case
231
+ keywords. `Subscription` fields resolve with their classes, as before.
232
+
179
233
  ### Naming a class explicitly
180
234
 
181
235
  `@field(resolver:)` points a field at a class by name. On `Query` and `Mutation`
@@ -228,11 +282,11 @@ What PHP Lighthouse hands a resolver as `$resolveInfo`. A `Resolvers::` or
228
282
  `enhance_builder` lets a class build the query no directive could, and still honour
229
283
  what the field declares: its `@builder` classes, and the argument directives the
230
284
  client used (`@eq`, `@where`, `@in`, `@like`, `@orderBy`, `@whereConditions`, ...),
231
- as `@all` and `@paginate` apply them. `scopes:` adds model scopes.
285
+ then the field's own `@orderBy(column:)`, as `@all` and `@paginate` apply them. `scopes:` adds model scopes.
232
286
 
233
287
  ```graphql
234
288
  type Query {
235
- myPosts(title: String @like, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @guard
289
+ myPosts(title: String @like, orderBy: _ @orderBy(columns: ["title", "created_at"])): [Post!]! @guard
236
290
  }
237
291
  ```
238
292
 
@@ -317,8 +371,9 @@ what every resolver shares.
317
371
 
318
372
  graphql-ruby keeps the Ruby side of each object type in `app/graphql/types`:
319
373
  `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
374
+ Post` writes it. Lighthouse keeps that structure. A type class that includes
375
+ `Lighthouse::GraphQL::Type` is handed to Lighthouse. After `lighthouse:install`,
376
+ `Types::BaseObject` includes it, so every type class does: the SDL still declares the type and its fields, and the class says how
322
377
  the fields with no resolving directive resolve. A field calls the method of its
323
378
  snake_case name, when the class defines it. Fields it does not define are read off
324
379
  the object, as before, and fields with a resolving directive keep it.
@@ -343,8 +398,6 @@ type ReadingTime {
343
398
  # app/graphql/types/post_type.rb
344
399
  module Types
345
400
  class PostType < BaseObject
346
- include Lighthouse::GraphQL::Type
347
-
348
401
  def reading_time
349
402
  { words: object.body.to_s.split.size }
350
403
  end
@@ -354,8 +407,6 @@ end
354
407
  # app/graphql/types/reading_time_type.rb
355
408
  module Types
356
409
  class ReadingTimeType < BaseObject
357
- include Lighthouse::GraphQL::Type
358
-
359
410
  def minutes(words_per_minute:)
360
411
  [(object[:words] / words_per_minute.to_f).ceil, 1].max
361
412
  end
@@ -403,9 +454,11 @@ interface to `Article` ([types.md](types.md#the-default)).
403
454
 
404
455
  The class is the type's name plus `Type`, looked up in `namespaces[:types]`
405
456
  (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.
457
+ `Lighthouse::GraphQL::Type` counts: any other `Types::PostType` is left alone, and
458
+ its methods resolve nothing. `lighthouse:install` includes the concern in
459
+ `Types::BaseObject`, `BaseInterface`, `BaseUnion` and `BaseScalar`, which hands every
460
+ type class over at once. In an app that did not run it, include the concern in
461
+ those bases, or in each type class (`include Lighthouse::GraphQL::Type`).
409
462
 
410
463
  A class that includes the concern must inherit from the schema's
411
464
  `base_types[:object]` when one is given, or the schema does not build. The concern
@@ -413,9 +466,9 @@ also hands over the classes of unions, interfaces and scalars, which pick the ty
413
466
  of a union's values or coerce a scalar's: see [types.md](types.md). It is not the schema's type: it only lends its
414
467
  methods. Fields are declared in the SDL, so `field` declarations in it are ignored,
415
468
  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.
469
+ [`@can*`](authorization.md). `Query` and `Mutation` are the exception: they are
470
+ built on their [root type classes](#root-type-classes), whose `field`s count.
471
+ `Subscription` fields resolve with their own classes.
419
472
 
420
473
  ### `@spread` and `@inject`
421
474
 
@@ -476,18 +529,18 @@ end
476
529
  ## Generators
477
530
 
478
531
  ```shell
479
- bin/rails generate lighthouse:query LatestPosts # app/graphql/resolvers/latest_posts.rb
480
- bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/publish_post.rb
481
532
  bin/rails generate lighthouse:subscription PostUpdated # app/graphql/subscriptions/post_updated.rb
482
533
  bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
483
534
  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
535
+ bin/rails generate graphql:object Post # a type class, Lighthouse's through Types::BaseObject
536
+ bin/rails generate graphql:mutation PublishPost # a code-first mutation, a field of Types::MutationType
485
537
  ```
486
538
 
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,
539
+ Each Lighthouse generator writes the class with its contract, into the first
540
+ namespace configured for its kind (or `--namespace=…`). Type classes come from
541
+ graphql-ruby's own `graphql:object`, `graphql:scalar`, `graphql:union` and
542
+ `graphql:interface`. A resolver class has no generator, as in graphql-ruby: write
543
+ it by hand, as above. [generators.md](generators.md) has what each generator writes,
491
544
  and which of graphql-ruby's fit a Lighthouse app.
492
545
 
493
546
  ## Changing the namespaces
@@ -0,0 +1,177 @@
1
+ # Schema Organisation
2
+
3
+ Lighthouse reads your schema from a single entrypoint, `graphql/schema.graphql`
4
+ by default (`config.schema_path`), as PHP Lighthouse does. As the schema grows,
5
+ you split it across files, and the entrypoint imports them.
6
+
7
+ ## Schema imports
8
+
9
+ ```
10
+ graphql/
11
+ |-- schema.graphql
12
+ |-- user.graphql
13
+ ```
14
+
15
+ An import starts with `#import`, followed by the path of the imported file,
16
+ relative to the file the import is in:
17
+
18
+ ```graphql
19
+ type Query {
20
+ user: User
21
+ }
22
+
23
+ #import user.graphql
24
+ ```
25
+
26
+ The contents of `user.graphql` are pasted in where the import is:
27
+
28
+ ```graphql
29
+ type Query {
30
+ user: User
31
+ }
32
+
33
+ type User {
34
+ name: String!
35
+ }
36
+ ```
37
+
38
+ An imported file can import others. A file is read once, so importing it twice,
39
+ or importing a file that imports it back, adds nothing more.
40
+
41
+ Only the entrypoint and the files it imports make up the schema. A `.graphql`
42
+ file in the folder that nothing imports is not part of it. A missing import
43
+ fails the build: "Did not find GraphQL schema import at …".
44
+
45
+ ### Wildcard imports
46
+
47
+ ```
48
+ graphql/
49
+ |-- schema.graphql
50
+ |-- blog/
51
+ |-- post.graphql
52
+ |-- category.graphql
53
+ ```
54
+
55
+ To import every file in `blog/` at once, use a glob (Ruby's `Dir.glob`), which
56
+ imports the files in name order:
57
+
58
+ ```graphql
59
+ #import blog/*.graphql
60
+ ```
61
+
62
+ `#import **/*.graphql` imports every file below the entrypoint's folder.
63
+
64
+ ## Type extensions
65
+
66
+ A type is defined once. Defining it twice is an error that points at
67
+ `extend`. This applies even across files:
68
+
69
+ ```
70
+ Duplicate definition Query found when parsing the schema. Define a type once and add to it from other files with `extend type Query`.
71
+ ```
72
+
73
+ PHP Lighthouse keeps the last definition and drops the earlier one without a
74
+ word. Lighthouse here makes that an error, so a type's fields are never lost.
75
+
76
+ Say `Post` is defined in `post.graphql`:
77
+
78
+ ```graphql
79
+ type Post {
80
+ title: String
81
+ author: User @belongsTo
82
+ }
83
+ ```
84
+
85
+ and `schema.graphql` imports it:
86
+
87
+ ```graphql
88
+ #import post.graphql
89
+ ```
90
+
91
+ To add the queries that fetch posts, you could add them to the `Query` type in
92
+ `schema.graphql`. It is usually better to keep queries next to the type they
93
+ return. Make sure `schema.graphql` defines `Query`, even with no fields:
94
+
95
+ ```graphql
96
+ type Query
97
+ ```
98
+
99
+ Then extend `Query` in `post.graphql`:
100
+
101
+ ```graphql
102
+ type Post {
103
+ title: String
104
+ author: User @belongsTo
105
+ }
106
+
107
+ extend type Query {
108
+ posts: [Post!]! @paginate
109
+ }
110
+ ```
111
+
112
+ The fields of the `extend type` are merged into the type's definition, and the
113
+ directives on them work as on any field. Besides object types, you can extend
114
+ `input`, `interface`, `enum`, `union` and `scalar` types:
115
+
116
+ ```graphql
117
+ extend input PostFilter { authorId: ID @eq(key: "author_id") }
118
+ extend enum Role { EDITOR }
119
+ extend union SearchResult = Comment
120
+ ```
121
+
122
+ The final schema always has one type per name. An extension of a type that is
123
+ not defined is an error, and so is an extension that redefines a field:
124
+
125
+ ```
126
+ Could not find a base definition User of kind ObjectTypeExtension to extend.
127
+ Duplicate definition title found when merging.
128
+ ```
129
+
130
+ The root types `Query`, `Mutation` and `Subscription` are the exception: an
131
+ `extend type Mutation` with no `type Mutation` anywhere defines it.
132
+
133
+ ## How the schema is built
134
+
135
+ The build runs these phases in order, as PHP Lighthouse's `ASTBuilder` does:
136
+
137
+ 1. The entrypoint and its imports are read (`Lighthouse::GraphQL::SchemaStitcher`),
138
+ along with what `BuildSchemaString` listeners add
139
+ ([events.md](events.md)).
140
+ 2. Every type is defined once (`Lighthouse::GraphQL::DocumentAST`).
141
+ 3. Type extensions are merged into their types.
142
+ 4. Type and field manipulators run: `@hide`/`@show`, `@model`, `@paginate`
143
+ and the relation directives' `type:`, `@softDeletes`, `@node`.
144
+ 5. Argument manipulators run: `@whereConditions`, `@whereHasConditions` and
145
+ `@orderBy` replace the `_` placeholder with the types they generate
146
+ ([filtering-and-ordering.md](filtering-and-ordering.md)). An argument still
147
+ typed `_` after this phase is an error.
148
+ 6. `ManipulateAST` listeners see the document.
149
+
150
+ ## IDE support
151
+
152
+ An IDE reads the `.graphql` files as they are written, so it does not know
153
+ Lighthouse's directives, the types the build generates, or the `_` placeholder.
154
+ Lighthouse writes two files that declare them:
155
+
156
+ - `schema-directives.graphql`: every directive the schema uses and does not declare.
157
+ - `programmatic-types.graphql`: the generated types (paginators, the `@orderBy`
158
+ and `@whereConditions` inputs, ...) and `scalar _`.
159
+
160
+ `graphql.config.yml` (written by `lighthouse:install`) points the GraphQL
161
+ plugins of VS Code and JetBrains IDEs at them, next to every `.graphql` file:
162
+
163
+ ```yaml
164
+ schema:
165
+ - graphql/**/*.graphql
166
+ - schema-directives.graphql
167
+ - programmatic-types.graphql
168
+ ```
169
+
170
+ In development, Lighthouse rewrites both files on boot and on every reload when
171
+ the app has a `graphql.config.yml` (`config.ide_helper`). Elsewhere, run
172
+ `bin/rails lighthouse:ide_helper`. No type is in two of the three sources, because
173
+ graphql-js, which the IDE plugins build the schema with, rejects a type defined
174
+ twice.
175
+
176
+ The IDE sees the files as one schema, imports or not. Without the generated
177
+ files it reports `Unknown type "_"` and unknown directives.
data/docs/security.md CHANGED
@@ -10,20 +10,24 @@ Lighthouse works the same way on top of webonyx's validation rules.
10
10
 
11
11
  ## Setting the limits
12
12
 
13
- The limits are graphql-ruby's own schema settings. Set them on the schema
14
- Lighthouse builds:
13
+ The limits are graphql-ruby's own schema settings. Set them in the body of the
14
+ schema class, as in any graphql-ruby schema (graphql-ruby's install writes
15
+ `max_depth(15)` there):
15
16
 
16
17
  ```ruby
17
18
  # app/graphql/app_schema.rb
18
- AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(options: { using: { GraphQL::Dataloader => {} } })
19
+ class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { object: Types::BaseObject } })
20
+ use GraphQL::Dataloader
19
21
 
20
- AppSchema.max_complexity(300) # refuse queries that score over 300
21
- AppSchema.max_depth(12) # refuse queries nested deeper than 12 fields
22
- AppSchema.disable_introspection_entry_points if Rails.env.production?
22
+ max_complexity(300) # refuse queries that score over 300
23
+ max_depth(12) # refuse queries nested deeper than 12 fields
24
+ disable_introspection_entry_points if Rails.env.production?
25
+ end
23
26
  ```
24
27
 
25
- You can also set them on a base schema class you hand to `get_schema` with
26
- `options: { base_schema: MyBaseSchema }`.
28
+ A schema assigned to a constant (`AppSchema = get_schema(...)`) takes them as calls
29
+ on it (`AppSchema.max_complexity(300)`). You can also set them on a base schema class
30
+ you hand to `get_schema` with `options: { base_schema: MyBaseSchema }`.
27
31
 
28
32
  A query over the limit isn't executed. Its response holds only the error:
29
33
 
@@ -211,17 +215,61 @@ GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryComplexity]).fir
211
215
 
212
216
  ## Rate limiting
213
217
 
214
- [`@throttle`](directives.md#throttle) caps how often one client may call a field —
215
- a login, a search, an expensive mutation:
218
+ Rails' own [`rate_limit`](https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html)
219
+ limits the endpoint: every operation counts, and past the limit Rails answers 429.
220
+ [`@throttle`](directives.md#throttle) is the same limit on one field — a login, a search,
221
+ an expensive mutation — beside it:
222
+
223
+ ```ruby
224
+ class GraphqlController < ApplicationController
225
+ include Lighthouse::GraphQL::Controller
226
+
227
+ lighthouse_schema AppSchema
228
+ rate_limit to: 600, within: 1.minute, name: 'endpoint'
229
+
230
+ private
231
+
232
+ def too_many_requests
233
+ render json: { errors: [{ message: 'Too many requests. Try again later.' }] }, status: :too_many_requests
234
+ end
235
+ end
236
+ ```
216
237
 
217
238
  ```graphql
218
239
  type Mutation {
219
- login(email: String!, password: String!): User @throttle(maxAttempts: 5, decayMinutes: 1)
240
+ login(email: String!, password: String!): User @throttle(maxAttempts: 5)
241
+ createTag(name: String!): Tag! @throttle(maxAttempts: 20, by: "current_user.id", with: "too_many_requests") @guard @create
220
242
  }
221
243
  ```
222
244
 
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.
245
+ `@throttle` counts as `rate_limit` does, under its key (`rate-limit:<scope>:<name>:<by>`):
246
+
247
+ - **Where.** In the controller's `cache_store`: `config.action_controller.cache_store`,
248
+ or `Rails.cache` when the app sets none (a controller can set its own with
249
+ `self.cache_store =`). Use a store the app servers share (Redis, Solid Cache) in
250
+ production; `:null_store` counts nothing.
251
+ - **Who.** The client's IP, as `rate_limit`'s default. `by:` reads another value off the
252
+ context, as `@inject` does (`by: "current_user.id"`); when it is `nil`, as for a guest,
253
+ those calls count together, as in Rails.
254
+ - **What counts together.** Each field counts under its own name (`Query.login`), apart
255
+ from the endpoint's `rate_limit` (`endpoint`). Give fields, or a field and a
256
+ controller's `rate_limit`, the same `name:` to count them together; `prefix:` is
257
+ `rate_limit`'s `scope:` (the controller's path by default).
258
+ - **Past the limit.** As `rate_limit`'s default, `ActionController::TooManyRequests` is
259
+ raised, and Rails answers the request with a 429 (in development too); on Rails 7.2,
260
+ which has no such error, `head :too_many_requests`. With `with:`, the controller's
261
+ method runs instead, as `rate_limit`'s `with:`. If it renders or redirects, the request
262
+ ends with that response and the operation stops there, as when a `before_action`
263
+ renders. If it returns, the call goes through. If it raises a `GraphQL::ExecutionError`,
264
+ only that field fails, and the rest of the operation is answered:
265
+
266
+ ```ruby
267
+ def rate_limited = raise(GraphQL::ExecutionError, 'Too many requests. Try again later.')
268
+ ```
269
+
270
+ Without Action Pack, the field fails (`Rate limit for Query.x exceeded. Try again later.`).
271
+ - **Instrumentation.** Each refused call is a `rate_limit.action_controller` event, with
272
+ `rate_limit`'s payload (`request`, `count`, `to`, `within`, `by`, `name`, `scope`,
273
+ `cache_key`).
274
+
275
+ A refused call never reaches `@guard` or the resolver.
@@ -38,18 +38,24 @@ end
38
38
 
39
39
  graphql-ruby delivers subscriptions through a *backend* that stores who subscribed
40
40
  and sends the updates: ActionCable (bundled), or a gem such as Pusher or Ably.
41
- Lighthouse needs none of its own; give the schema one. `get_schema` passes the
42
- `using:` option to graphql-ruby, as `use` does in a class-based schema:
41
+ Lighthouse needs none of its own; give the schema one with `use`, in the schema
42
+ class body, as in any graphql-ruby schema:
43
43
 
44
44
  ```ruby
45
45
  # app/graphql/app_schema.rb
46
- AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
47
- options: { using: { GraphQL::Subscriptions::ActionCableSubscriptions => {} } }
46
+ class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
47
+ options: { base_types: { subscription: Subscriptions::BaseSubscription } } # and the other bases
48
48
  )
49
+ use GraphQL::Dataloader
50
+ use GraphQL::Subscriptions::ActionCableSubscriptions
51
+ end
49
52
  ```
50
53
 
51
- Options of the backend go in the hash (`{ ActionCableSubscriptions => { namespace: 'my-app' } }`).
52
- Assign the schema to a constant: a [queued](#queueing) broadcast finds it by name.
54
+ Options of the backend go after it (`use GraphQL::Subscriptions::ActionCableSubscriptions, namespace: 'my-app'`).
55
+ A schema assigned to a constant instead (`AppSchema = get_schema(...)`) takes the
56
+ backend through the `using:` option, as `use` does:
57
+ `options: { using: { GraphQL::Subscriptions::ActionCableSubscriptions => {} } }`.
58
+ Either way the schema has a name, which a [queued](#queueing) broadcast finds it by.
53
59
 
54
60
  Then add the channel graphql-ruby documents, which runs the operation and keeps
55
61
  the client's subscription ids ([the full example](https://graphql-ruby.org/subscriptions/action_cable_implementation.html)):
@@ -182,7 +188,7 @@ the base every subscription inherits from, as graphql-ruby's convention puts wha
182
188
  all classes of a kind share in their base:
183
189
 
184
190
  ```ruby
185
- # app/graphql/subscriptions/base_subscription.rb (rails generate lighthouse:install)
191
+ # app/graphql/subscriptions/base_subscription.rb (rails generate lighthouse:install --subscriptions)
186
192
  module Subscriptions
187
193
  class BaseSubscription < GraphQL::Schema::Subscription
188
194
  include Lighthouse::GraphQL::Subscription
@@ -411,8 +417,9 @@ end
411
417
 
412
418
  The job carries the schema's name, the subscription's name, the trigger arguments,
413
419
  the scope and the root; an Active Record model is serialized with GlobalID, any
414
- other root must be something ActiveJob can serialize. A schema built by
415
- `get_schema` and assigned to a constant (`AppSchema = ...`) has the name; an
420
+ other root must be something ActiveJob can serialize. A schema class
421
+ (`class AppSchema < ...get_schema(...)`), or a schema assigned to a constant
422
+ (`AppSchema = ...`), has the name; an
416
423
  anonymous one raises `Lighthouse::DefinitionError` when a queued broadcast needs
417
424
  it. The `activejob` gem is optional: nothing loads it unless a broadcast is queued.
418
425