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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -1
- data/README.md +38 -22
- data/docs/best-practices.md +6 -4
- data/docs/caching.md +4 -3
- data/docs/configuration.md +20 -26
- data/docs/custom-directives.md +2 -1
- data/docs/directives.md +79 -14
- data/docs/federation.md +1 -1
- data/docs/filtering-and-ordering.md +330 -30
- data/docs/generators.md +42 -40
- data/docs/getting-started.md +128 -58
- data/docs/motivation.md +3 -2
- data/docs/performance.md +5 -4
- data/docs/relationships.md +7 -5
- data/docs/relay.md +63 -0
- data/docs/resolvers.md +92 -39
- data/docs/schema-organisation.md +177 -0
- data/docs/security.md +64 -16
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +16 -17
- data/docs/uploads.md +41 -14
- data/lib/generators/lighthouse/base_classes.rb +9 -14
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -99
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
- data/lib/lighthouse/ast_builder.rb +124 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/config.rb +27 -25
- data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
- data/lib/lighthouse/contracts/field_builder.rb +13 -0
- data/lib/lighthouse/controller.rb +28 -5
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
- data/lib/lighthouse/directive_resolvers/base.rb +6 -37
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
- data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
- data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
- data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
- data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
- data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
- data/lib/lighthouse/directives/definitions.rb +0 -1
- data/lib/lighthouse/directives/generated_types.rb +103 -0
- data/lib/lighthouse/directives/registry.rb +39 -10
- data/lib/lighthouse/document_ast.rb +111 -0
- data/lib/lighthouse/field_resolver_stack.rb +9 -6
- data/lib/lighthouse/global_id.rb +29 -3
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- data/lib/lighthouse/ide_helper.rb +80 -23
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/order_by/types.rb +137 -0
- data/lib/lighthouse/railtie.rb +9 -2
- data/lib/lighthouse/rb_lighthouse.rb +16 -5
- data/lib/lighthouse/resolve_info.rb +5 -5
- data/lib/lighthouse/resolver_bases.rb +18 -4
- data/lib/lighthouse/resolver_dispatch.rb +16 -1
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/schema_cache.rb +8 -4
- data/lib/lighthouse/schema_factory.rb +6 -9
- data/lib/lighthouse/schema_generator.rb +8 -18
- data/lib/lighthouse/schema_stitcher.rb +64 -0
- data/lib/lighthouse/support/naming.rb +0 -12
- data/lib/lighthouse/upload.rb +4 -2
- data/lib/lighthouse/upload_store.rb +39 -14
- data/lib/lighthouse/where_conditions/columns.rb +36 -0
- data/lib/lighthouse/where_conditions/handler.rb +152 -0
- data/lib/lighthouse/where_conditions/json_column.rb +99 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
- data/lib/lighthouse/where_conditions/types.rb +110 -0
- data/sig/lighthouse/directives.rbs +16 -3
- data/sig/lighthouse/graphql.rbs +48 -4
- data/sig/lighthouse.rbs +8 -12
- metadata +17 -7
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
- data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
- 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` (
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
37
|
+
# app/graphql/mutations/base_mutation.rb (graphql-ruby's)
|
|
37
38
|
module Mutations
|
|
38
|
-
class BaseMutation < GraphQL::Schema::
|
|
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
|
|
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`
|
|
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
|
|
153
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
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:
|
|
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
|
|
321
|
-
Lighthouse
|
|
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
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
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
|
|
488
|
-
kind (or `--namespace=…`)
|
|
489
|
-
|
|
490
|
-
`graphql:interface`.
|
|
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
|
|
14
|
-
|
|
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
|
|
19
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { object: Types::BaseObject } })
|
|
20
|
+
use GraphQL::Dataloader
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
`
|
|
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
|
-
[
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
data/docs/subscriptions.md
CHANGED
|
@@ -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
|
|
42
|
-
|
|
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
|
|
47
|
-
options: {
|
|
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
|
|
52
|
-
|
|
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
|
|
415
|
-
`get_schema`
|
|
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
|
|