lighthouse-graphql 0.3.0 → 0.4.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 +44 -1
- data/README.md +11 -3
- data/docs/best-practices.md +16 -6
- data/docs/configuration.md +7 -6
- data/docs/directives.md +31 -6
- data/docs/events.md +3 -1
- data/docs/federation.md +3 -2
- data/docs/getting-started.md +52 -14
- data/docs/motivation.md +3 -2
- data/docs/resolvers.md +93 -19
- data/docs/security.md +210 -0
- data/docs/subscriptions.md +60 -37
- data/lib/generators/lighthouse/base_classes.rb +87 -0
- data/lib/generators/lighthouse/class_generator.rb +19 -8
- data/lib/generators/lighthouse/install/install_generator.rb +23 -6
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +7 -6
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +14 -2
- data/lib/generators/lighthouse/mutation_generator.rb +5 -4
- data/lib/generators/lighthouse/query_generator.rb +5 -4
- data/lib/generators/lighthouse/subscription_generator.rb +21 -16
- data/lib/lighthouse/complexity.rb +174 -0
- data/lib/lighthouse/complexity_analyzer.rb +56 -0
- data/lib/lighthouse/config.rb +4 -3
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +14 -4
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +12 -4
- data/lib/lighthouse/directives/registry.rb +1 -0
- data/lib/lighthouse/events/execution_trace.rb +2 -1
- data/lib/lighthouse/events/lifecycle.rb +3 -1
- data/lib/lighthouse/field_resolver_stack.rb +8 -7
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/rb_lighthouse.rb +3 -0
- data/lib/lighthouse/resolver_bases.rb +61 -0
- data/lib/lighthouse/schema_factory.rb +0 -1
- data/lib/lighthouse/schema_generator.rb +15 -7
- data/lib/lighthouse/sdl_loader.rb +0 -2
- data/lib/lighthouse/support/model_resolver.rb +1 -1
- data/sig/lighthouse/graphql.rbs +29 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a15bb4d92716a08d5d248c5155b9cf336407a7988fed6e00c825b07bea4a206b
|
|
4
|
+
data.tar.gz: 421fca84187c5030f38c2cdd07ed26f9909747944f788cfd9662e67b6e322e2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 388112925823baa63d0a7a4436bb1df4511f4a4c13b01001dd72b351156b78982ca4eab136a131c9c25b997915bff26099fa2a485bc4df74bfef390138e37c3a
|
|
7
|
+
data.tar.gz: e2f90e9654cfcdc2d26e084f68476ac59f56eb708683b3b4817cfc41006843363feb0f9a08b838bdcdd888395bc8f13ae29394d28b0bc37566c26dad63657cac
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,48 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-10-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `@complexity` sets what a field costs in graphql-ruby's query complexity analysis, so
|
|
14
|
+
`AppSchema.max_complexity(n)` refuses expensive queries before they run. A bare `@complexity`
|
|
15
|
+
costs `1 + child complexity × first`. `@complexity(resolver: "Name")` names a
|
|
16
|
+
`GraphQL::Analysis::Analyzer` subclass in the new `analyzers` namespace (`Analyzers::Name`),
|
|
17
|
+
which Lighthouse installs on the schema. Including `Lighthouse::GraphQL::ComplexityAnalyzer`
|
|
18
|
+
gives the analyzer a `complexity` (a number, or a proc of `context, args, child_complexity`,
|
|
19
|
+
as graphql-ruby's) that is the cost of the fields naming it.
|
|
20
|
+
- A root field resolved by a `GraphQL::Schema::Resolver` class, by convention or through `@field`,
|
|
21
|
+
takes the `complexity` that class declares, as graphql-ruby's `field ..., resolver:` does.
|
|
22
|
+
- `BuildExtensionsResponse#query_complexity` holds the query's complexity score while something
|
|
23
|
+
listens to the event. It used to be `nil`.
|
|
24
|
+
- `docs/security.md`: query complexity, depth and introspection limits.
|
|
25
|
+
- graphql-ruby's base classes: `rails g lighthouse:install` writes `Resolvers::BaseResolver`,
|
|
26
|
+
`Mutations::BaseMutation` and `Subscriptions::BaseSubscription` (which includes
|
|
27
|
+
`Lighthouse::GraphQL::Subscription`), and `lighthouse:query`, `:mutation` and `:subscription`
|
|
28
|
+
write the one they need when it is missing. Generated classes inherit from them. An existing
|
|
29
|
+
base, such as the ones graphql-ruby's `graphql:install` writes, is never replaced.
|
|
30
|
+
- `get_schema`'s `base_types` takes `resolver:`, `mutation:` and `subscription:` beside
|
|
31
|
+
graphql-ruby's own keys. Every class a `Query`, `Mutation` or `Subscription` field resolves to
|
|
32
|
+
must then inherit from its kind's base (`Lighthouse::DefinitionError` otherwise), and a
|
|
33
|
+
`Subscription` field with no class of its own resolves with the subscription base. The
|
|
34
|
+
generated schema class names the three.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- The classes of `Query` fields are looked up in `Resolvers` (`app/graphql/resolvers`), graphql-ruby's
|
|
39
|
+
convention, instead of `Queries`: `Query.latestPosts` resolves to `Resolvers::LatestPosts`, and
|
|
40
|
+
`lighthouse:query` writes there. To keep `app/graphql/queries`, set
|
|
41
|
+
`config.namespaces = { queries: ['Queries'] }`.
|
|
42
|
+
- `lighthouse:subscription` no longer takes `--lighthouse`. The class inherits from
|
|
43
|
+
`Subscriptions::BaseSubscription`, which includes the concern, and gets its hooks; under a base
|
|
44
|
+
without the concern, it gets graphql-ruby's `authorized?` and `update`.
|
|
45
|
+
|
|
46
|
+
- Paginated fields (`@paginate`, and relationship directives given `type:`) cost
|
|
47
|
+
`1 + child complexity × first` in the complexity score. A page of 1,000 items now scores
|
|
48
|
+
1,000 times one item's cost; before, a paginator scored the same whatever `first` was.
|
|
49
|
+
A schema that already sets `max_complexity` may need a higher limit.
|
|
50
|
+
|
|
9
51
|
## [0.3.0] - 2026-10-17
|
|
10
52
|
|
|
11
53
|
### Added
|
|
@@ -282,7 +324,8 @@ had been running inside a private monorepo as a path gem before being extracted.
|
|
|
282
324
|
- Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
|
|
283
325
|
`on_error`, and `DirectiveRegistry.register` for app-defined directives.
|
|
284
326
|
|
|
285
|
-
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.
|
|
327
|
+
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.4.0...HEAD
|
|
328
|
+
[0.4.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.3.0...v0.4.0
|
|
286
329
|
[0.3.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.1...v0.3.0
|
|
287
330
|
[0.2.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.0...v0.2.1
|
|
288
331
|
[0.2.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.1...v0.2.0
|
data/README.md
CHANGED
|
@@ -127,7 +127,14 @@ Prefer to wire it by hand? These are the steps the generator performs.
|
|
|
127
127
|
|
|
128
128
|
AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
129
129
|
sdl_folder: Rails.root.join('graphql').to_s,
|
|
130
|
-
options: {
|
|
130
|
+
options: {
|
|
131
|
+
base_types: {
|
|
132
|
+
resolver: Resolvers::BaseResolver, # graphql-ruby's base classes, which
|
|
133
|
+
mutation: Mutations::BaseMutation, # rails g lighthouse:install writes
|
|
134
|
+
subscription: Subscriptions::BaseSubscription
|
|
135
|
+
# object: Types::BaseObject, ...
|
|
136
|
+
}
|
|
137
|
+
}
|
|
131
138
|
)
|
|
132
139
|
```
|
|
133
140
|
|
|
@@ -143,7 +150,8 @@ including base types and configuration.
|
|
|
143
150
|
### Example app
|
|
144
151
|
|
|
145
152
|
[`example/`](example) is a small blog API built with Lighthouse on Rails 8.1. It shows
|
|
146
|
-
the schema, the generated `
|
|
153
|
+
the schema, the generated `Resolvers::`/`Mutations::`/`Subscriptions::`/`Builders::`/`Validators::` classes
|
|
154
|
+
under graphql-ruby's base classes,
|
|
147
155
|
Pundit policies and the opt-in performance features working together, and its specs
|
|
148
156
|
are the library's end-to-end suite. See [example/README.md](example/README.md).
|
|
149
157
|
|
|
@@ -161,7 +169,7 @@ or as Markdown in [`docs/`](docs):
|
|
|
161
169
|
| [Directives reference](docs/directives.md) | Every built-in directive, grouped by purpose |
|
|
162
170
|
| [Relationships](docs/relationships.md) | `@hasMany`, `@belongsTo`, `@belongsToMany`, `@hasOne`, the `@morph*` variants, batching and `type:` pagination |
|
|
163
171
|
| [Filtering & ordering](docs/filtering-and-ordering.md) | `@eq`/`@where`/`@in`/`@like`, `@whereConditions`, `@orderBy` |
|
|
164
|
-
| [
|
|
172
|
+
| [Resolvers, mutations & builders](docs/resolvers.md) | `Resolvers::` / `Mutations::` classes and their base classes (graphql-ruby's contract), builders, generators |
|
|
165
173
|
| [Mutations](docs/mutations.md) | `@create`, `@update`, `@upsert`, `@delete` (and `*Many`), nested mutations on every kind of association, `@spread`, `@inject` |
|
|
166
174
|
| [Validation](docs/validation.md) | Model validations, `@validator` with Dry::Validation contracts |
|
|
167
175
|
| [Authentication & authorization](docs/authorization.md) | `@guard`, `@canFind` / `@canModel` / `@canQuery` / `@canResolved` / `@canRoot` (Pundit by default) |
|
data/docs/best-practices.md
CHANGED
|
@@ -44,12 +44,22 @@ Use `@guard` and the `@can*` directives on fields that require them. They wrap t
|
|
|
44
44
|
resolver, so the check runs before any data is loaded (only `@canResolved`, which
|
|
45
45
|
needs the result, runs after — keep it to fields that don't write).
|
|
46
46
|
|
|
47
|
-
## Keep
|
|
48
|
-
|
|
49
|
-
A custom `Query` field is a `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
contracts;
|
|
47
|
+
## Keep resolvers, mutations and builders apart
|
|
48
|
+
|
|
49
|
+
A custom `Query` field is a `Resolvers::BaseResolver` in `app/graphql/resolvers`, a
|
|
50
|
+
`Mutation` field a `Mutations::BaseMutation` in `app/graphql/mutations`, and a query
|
|
51
|
+
refinement a builder in `app/graphql/builders`. They have different jobs and
|
|
52
|
+
contracts; keep each kind in its own folder. See [resolvers.md](resolvers.md).
|
|
53
|
+
|
|
54
|
+
## Share through the base classes
|
|
55
|
+
|
|
56
|
+
What every resolver, mutation or subscription needs (a concern, a helper,
|
|
57
|
+
graphql-ruby's `argument_class`) goes in `Resolvers::BaseResolver`,
|
|
58
|
+
`Mutations::BaseMutation` or `Subscriptions::BaseSubscription`, once, as in any
|
|
59
|
+
graphql-ruby app. Name them in the schema's `base_types` (`resolver:`, `mutation:`,
|
|
60
|
+
`subscription:`), and a class that forgets to inherit from its base is refused
|
|
61
|
+
instead of quietly missing what it shares. See
|
|
62
|
+
[resolvers.md](resolvers.md#base-classes).
|
|
53
63
|
|
|
54
64
|
## Validate in the model first
|
|
55
65
|
|
data/docs/configuration.md
CHANGED
|
@@ -10,8 +10,8 @@ require 'lighthouse-graphql'
|
|
|
10
10
|
|
|
11
11
|
Lighthouse.configure do |config|
|
|
12
12
|
config.sdl_folder = Rails.root.join('graphql').to_s
|
|
13
|
-
config.namespaces = { queries: ['
|
|
14
|
-
config.resolver_namespaces = ['App::Graphql'
|
|
13
|
+
config.namespaces = { queries: ['Resolvers'], mutations: ['Mutations'] }
|
|
14
|
+
config.resolver_namespaces = ['App::Graphql']
|
|
15
15
|
config.on_error = ->(error, _context) { Rails.error.report(error) }
|
|
16
16
|
end
|
|
17
17
|
```
|
|
@@ -31,15 +31,16 @@ different contracts, so they live apart ([resolvers.md](resolvers.md)):
|
|
|
31
31
|
|
|
32
32
|
| Kind | Default | Holds |
|
|
33
33
|
| --- | --- | --- |
|
|
34
|
-
| `queries` | `['
|
|
35
|
-
| `mutations` | `['Mutations']` | `GraphQL::Schema::Mutation`
|
|
34
|
+
| `queries` | `['Resolvers']` | `Resolvers::BaseResolver` classes (a `GraphQL::Schema::Resolver`) for `Query` fields. `['Queries']` before 0.4.0 |
|
|
35
|
+
| `mutations` | `['Mutations']` | `Mutations::BaseMutation` classes (a `GraphQL::Schema::Mutation`) for `Mutation` fields |
|
|
36
36
|
| `builders` | `['Builders']` | `@builder` classes |
|
|
37
37
|
| `validators` | `['Validators']` | `@validator` contracts |
|
|
38
38
|
| `events` | `['Events']` | Classes `@event(dispatch:)` names ([events.md](events.md)) |
|
|
39
|
-
| `subscriptions` | `['Subscriptions']` | `GraphQL::Schema::Subscription
|
|
39
|
+
| `subscriptions` | `['Subscriptions']` | `Subscriptions::BaseSubscription` classes (a `GraphQL::Schema::Subscription`; the base includes `Lighthouse::GraphQL::Subscription`) for `Subscription` fields, and `@subscription(class:)` ([subscriptions.md](subscriptions.md)) |
|
|
40
|
+
| `analyzers` | `['Analyzers']` | `GraphQL::Analysis::Analyzer` classes (optionally including `Lighthouse::GraphQL::ComplexityAnalyzer`) that `@complexity(resolver:)` names ([security.md](security.md)) |
|
|
40
41
|
|
|
41
42
|
```ruby
|
|
42
|
-
config.namespaces = { queries: ['Api::
|
|
43
|
+
config.namespaces = { queries: ['Api::Resolvers'] } # the other kinds keep their defaults
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
### `resolver_namespaces`
|
data/docs/directives.md
CHANGED
|
@@ -150,8 +150,9 @@ The relation name defaults to the field name (snake_cased). Override with
|
|
|
150
150
|
|
|
151
151
|
Delegate a field to a class — your escape hatch for anything the built-in
|
|
152
152
|
directives don't cover. On `Query` and `Mutation` you usually don't need it:
|
|
153
|
-
a field without a resolving directive resolves to `
|
|
154
|
-
`Mutations::<FieldName>`, graphql-ruby resolver and mutation classes
|
|
153
|
+
a field without a resolving directive resolves to `Resolvers::<FieldName>` /
|
|
154
|
+
`Mutations::<FieldName>`, graphql-ruby resolver and mutation classes under
|
|
155
|
+
`Resolvers::BaseResolver` / `Mutations::BaseMutation`. See
|
|
155
156
|
[resolvers.md](resolvers.md).
|
|
156
157
|
|
|
157
158
|
```graphql
|
|
@@ -321,7 +322,7 @@ type Query {
|
|
|
321
322
|
### `@convertEmptyStringsToNull`
|
|
322
323
|
|
|
323
324
|
Replaces incoming empty strings `""` with `null`, before any resolver sees them —
|
|
324
|
-
directive resolvers (`@create`, `@all` filters, …), `@field`, `
|
|
325
|
+
directive resolvers (`@create`, `@all` filters, …), `@field`, `Resolvers::` /
|
|
325
326
|
`Mutations::` classes and `@validator` alike. On an argument or an input field it
|
|
326
327
|
converts that one input; on a field it converts every input of the field, nested
|
|
327
328
|
input objects and lists included. Non-null inputs (`String!`) pass unchanged when
|
|
@@ -520,9 +521,9 @@ directive @subscription(class: String!) on FIELD_DEFINITION
|
|
|
520
521
|
|
|
521
522
|
Names the class a `Subscription` field resolves to, when it does not follow the
|
|
522
523
|
convention (`postUpdated` → `Subscriptions::PostUpdated`). The class inherits from
|
|
523
|
-
`GraphQL::Schema::Subscription`
|
|
524
|
-
|
|
525
|
-
`Config.namespaces[:subscriptions]`; a name with `::` is used as it is.
|
|
524
|
+
`Subscriptions::BaseSubscription`, a `GraphQL::Schema::Subscription` that includes
|
|
525
|
+
`Lighthouse::GraphQL::Subscription` (PHP Lighthouse's `filter?` hooks), and is looked
|
|
526
|
+
up in `Config.namespaces[:subscriptions]`; a name with `::` is used as it is.
|
|
526
527
|
|
|
527
528
|
```graphql
|
|
528
529
|
type Subscription {
|
|
@@ -634,6 +635,30 @@ Flipper.enable_actor(:new_checkout, User.find(1))
|
|
|
634
635
|
|
|
635
636
|
---
|
|
636
637
|
|
|
638
|
+
## Query complexity
|
|
639
|
+
|
|
640
|
+
### `@complexity`
|
|
641
|
+
|
|
642
|
+
Customize what a field costs in the query's complexity score, which graphql-ruby
|
|
643
|
+
checks against the schema's `max_complexity` before it runs the query.
|
|
644
|
+
|
|
645
|
+
```graphql
|
|
646
|
+
type Query {
|
|
647
|
+
search(first: Int, term: String!): [Post!]! @complexity
|
|
648
|
+
posts(includeFullText: Boolean): [Post!]! @all @complexity(resolver: "PostsComplexity")
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
| Argument | Meaning |
|
|
653
|
+
| --- | --- |
|
|
654
|
+
| `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`. |
|
|
655
|
+
|
|
656
|
+
Paginated fields cost `1 + child complexity × first` without the directive, and a
|
|
657
|
+
field resolved by a `GraphQL::Schema::Resolver` class costs what that class
|
|
658
|
+
declares. See [security.md](security.md).
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
637
662
|
## Events
|
|
638
663
|
|
|
639
664
|
`@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
|
|
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
|
-
|
|
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
|
data/docs/getting-started.md
CHANGED
|
@@ -75,16 +75,44 @@ argument (the `@eq` makes `id` a filter). Lighthouse infers the model from the
|
|
|
75
75
|
field's return type name (`User`), or you can be explicit with
|
|
76
76
|
`@all(model: "User")`.
|
|
77
77
|
|
|
78
|
-
## 2. Provide your base
|
|
78
|
+
## 2. Provide your base classes
|
|
79
79
|
|
|
80
|
-
Lighthouse builds on graphql-ruby,
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
Lighthouse builds on graphql-ruby, and follows its convention: each kind of class
|
|
81
|
+
inherits from a base class in its own namespace, where what they all share goes
|
|
82
|
+
once. `rails g lighthouse:install` writes the bases of the classes that resolve
|
|
83
|
+
root fields:
|
|
83
84
|
|
|
84
85
|
```ruby
|
|
85
|
-
# app/graphql/
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
# app/graphql/resolvers/base_resolver.rb — Query fields (Resolvers::LatestPosts)
|
|
87
|
+
module Resolvers
|
|
88
|
+
class BaseResolver < GraphQL::Schema::Resolver
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# app/graphql/mutations/base_mutation.rb — Mutation fields (Mutations::PublishPost)
|
|
93
|
+
module Mutations
|
|
94
|
+
class BaseMutation < GraphQL::Schema::Mutation
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# app/graphql/subscriptions/base_subscription.rb — Subscription fields
|
|
99
|
+
module Subscriptions
|
|
100
|
+
class BaseSubscription < GraphQL::Schema::Subscription
|
|
101
|
+
include Lighthouse::GraphQL::Subscription
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The SDL's types can have graphql-ruby base classes too, as `graphql:install`
|
|
107
|
+
writes them: the place to mix in Apollo Federation or custom field and argument
|
|
108
|
+
behavior.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# app/graphql/types/base_object.rb
|
|
112
|
+
module Types
|
|
113
|
+
class BaseObject < GraphQL::Schema::Object
|
|
114
|
+
# include ApolloFederation::Object # if you use federation
|
|
115
|
+
end
|
|
88
116
|
end
|
|
89
117
|
```
|
|
90
118
|
|
|
@@ -98,13 +126,22 @@ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
|
98
126
|
sdl_folder: Rails.root.join('graphql').to_s,
|
|
99
127
|
options: {
|
|
100
128
|
base_types: {
|
|
101
|
-
|
|
129
|
+
resolver: Resolvers::BaseResolver,
|
|
130
|
+
mutation: Mutations::BaseMutation,
|
|
131
|
+
subscription: Subscriptions::BaseSubscription,
|
|
132
|
+
object: Types::BaseObject,
|
|
102
133
|
# interface:, union:, enum:, input_object:, scalar: ...
|
|
103
134
|
}
|
|
104
135
|
}
|
|
105
136
|
)
|
|
106
137
|
```
|
|
107
138
|
|
|
139
|
+
`object:` and the other SDL kinds go to graphql-ruby, which builds the SDL's types
|
|
140
|
+
from them. `resolver:`, `mutation:` and `subscription:` are Lighthouse's: every
|
|
141
|
+
class a `Query`, `Mutation` or `Subscription` field resolves to must inherit from
|
|
142
|
+
its kind's base, and a `Subscription` field with no class of its own resolves with
|
|
143
|
+
the subscription base. See [resolvers.md](resolvers.md#base-classes).
|
|
144
|
+
|
|
108
145
|
`get_schema` returns a `GraphQL::Schema` subclass you can use directly, or
|
|
109
146
|
subclass further (e.g. to add federation, tracing, a Dataloader):
|
|
110
147
|
|
|
@@ -247,18 +284,19 @@ type User {
|
|
|
247
284
|
}
|
|
248
285
|
```
|
|
249
286
|
|
|
250
|
-
## Custom
|
|
287
|
+
## Custom resolvers and mutations
|
|
251
288
|
|
|
252
289
|
When a field needs Ruby, write a graphql-ruby resolver or mutation class named
|
|
253
|
-
after it — `Query.latestPosts` resolves to `
|
|
254
|
-
(`app/graphql/
|
|
255
|
-
`Mutations::PublishPost
|
|
256
|
-
scaffolds one. See
|
|
290
|
+
after it — `Query.latestPosts` resolves to `Resolvers::LatestPosts`
|
|
291
|
+
(`app/graphql/resolvers/latest_posts.rb`, a `Resolvers::BaseResolver`),
|
|
292
|
+
`Mutation.publishPost` to `Mutations::PublishPost` (a `Mutations::BaseMutation`).
|
|
293
|
+
`bin/rails generate lighthouse:query LatestPosts` scaffolds one. See
|
|
294
|
+
[resolvers.md](resolvers.md).
|
|
257
295
|
|
|
258
296
|
## Next steps
|
|
259
297
|
|
|
260
298
|
- [Directives reference](directives.md) — the full vocabulary.
|
|
261
|
-
- [
|
|
299
|
+
- [Resolvers, mutations & builders](resolvers.md), [Mutations](mutations.md),
|
|
262
300
|
[Validation](validation.md), [Caching](caching.md).
|
|
263
301
|
- [Relationships](relationships.md) — wiring associations.
|
|
264
302
|
- [Filtering & ordering](filtering-and-ordering.md) — query constraints.
|
data/docs/motivation.md
CHANGED
|
@@ -151,8 +151,9 @@ Schema-first doesn't mean schema-only. When a field does something no directive
|
|
|
151
151
|
describes, write Ruby, using graphql-ruby's own classes:
|
|
152
152
|
|
|
153
153
|
- A `Query` or `Mutation` field without a resolving directive resolves by convention to
|
|
154
|
-
`
|
|
155
|
-
`
|
|
154
|
+
`Resolvers::FieldName` (a `Resolvers::BaseResolver`) or `Mutations::FieldName` (a
|
|
155
|
+
`Mutations::BaseMutation`), under the base classes graphql-ruby's convention gives
|
|
156
|
+
each kind. See [Resolvers, Mutations and Builders](resolvers.md).
|
|
156
157
|
- `@builder` classes adjust a query; `@validator` contracts check arguments.
|
|
157
158
|
- Your own directives plug in through the registry, with the same contracts as the
|
|
158
159
|
built-in ones. See [Custom Directives](custom-directives.md).
|
data/docs/resolvers.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Resolvers, Mutations and Builders
|
|
2
2
|
|
|
3
3
|
Most fields need no Ruby: a directive (`@all`, `@find`, `@paginate`, `@create`,
|
|
4
4
|
…) or the default attribute reader resolves them. For the rest, Lighthouse looks
|
|
@@ -7,17 +7,86 @@ interchangeable. Each kind has its own contract and its own folder:
|
|
|
7
7
|
|
|
8
8
|
| Kind | Folder / namespace | Contract | Used by |
|
|
9
9
|
| --- | --- | --- | --- |
|
|
10
|
-
|
|
|
11
|
-
| Mutation | `app/graphql/mutations` · `Mutations::` | `GraphQL::Schema::Mutation` | `Mutation` fields |
|
|
12
|
-
| Subscription | `app/graphql/subscriptions` · `Subscriptions::` | `GraphQL::Schema::Subscription` | `Subscription` fields ([subscriptions.md](subscriptions.md)) |
|
|
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 |
|
|
12
|
+
| Subscription | `app/graphql/subscriptions` · `Subscriptions::` | `Subscriptions::BaseSubscription` (a `GraphQL::Schema::Subscription`) | `Subscription` fields ([subscriptions.md](subscriptions.md)) |
|
|
13
13
|
| Builder | `app/graphql/builders` · `Builders::` | `call(relation, args, context)` → relation | `@builder` |
|
|
14
14
|
|
|
15
15
|
Validators (`app/graphql/validators`) are the fourth kind. They are covered in
|
|
16
16
|
[validation.md](validation.md).
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Base classes
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
As in any graphql-ruby app, each of the first three kinds has a base class in its
|
|
21
|
+
namespace, and every class of the kind inherits from it:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# app/graphql/resolvers/base_resolver.rb
|
|
25
|
+
module Resolvers
|
|
26
|
+
class BaseResolver < GraphQL::Schema::Resolver
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# app/graphql/mutations/base_mutation.rb
|
|
31
|
+
module Mutations
|
|
32
|
+
class BaseMutation < GraphQL::Schema::Mutation
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# app/graphql/subscriptions/base_subscription.rb
|
|
37
|
+
module Subscriptions
|
|
38
|
+
class BaseSubscription < GraphQL::Schema::Subscription
|
|
39
|
+
include Lighthouse::GraphQL::Subscription
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
What every class of a kind shares goes in its base, once: a concern (as
|
|
45
|
+
`BaseSubscription` includes Lighthouse's, see [subscriptions.md](subscriptions.md)),
|
|
46
|
+
a helper, graphql-ruby settings such as `argument_class Types::BaseArgument`.
|
|
47
|
+
`rails g lighthouse:install` writes the three, and the class generators write the
|
|
48
|
+
one they need when it is missing. graphql-ruby's own `graphql:install` writes
|
|
49
|
+
`Resolvers::BaseResolver` and `Mutations::BaseMutation` too, and Lighthouse keeps
|
|
50
|
+
whatever is already there: an existing base is never replaced.
|
|
51
|
+
|
|
52
|
+
### Handing the bases to the schema
|
|
53
|
+
|
|
54
|
+
Name the bases in `base_types`, beside graphql-ruby's own base types, and the
|
|
55
|
+
schema holds every class to them:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# app/graphql/app_schema.rb
|
|
59
|
+
AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
60
|
+
options: {
|
|
61
|
+
base_types: {
|
|
62
|
+
resolver: Resolvers::BaseResolver,
|
|
63
|
+
mutation: Mutations::BaseMutation,
|
|
64
|
+
subscription: Subscriptions::BaseSubscription
|
|
65
|
+
# object: Types::BaseObject, ... (graphql-ruby's, for the SDL's types)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- A class a `Query`, `Mutation` or `Subscription` field resolves to, by convention
|
|
72
|
+
or through `@subscription(class:)`, that does not inherit from its kind's base
|
|
73
|
+
raises `Lighthouse::DefinitionError` when the field is first used (`…so it must
|
|
74
|
+
inherit from Resolvers::BaseResolver…`). No class misses what the base shares.
|
|
75
|
+
- A `Subscription` field with no class of its own resolves with the subscription
|
|
76
|
+
base, so its concern and settings apply there too.
|
|
77
|
+
- A base must be a subclass of the graphql-ruby class of its kind
|
|
78
|
+
(`GraphQL::Schema::Resolver`, `GraphQL::Schema::Mutation`, which
|
|
79
|
+
`RelayClassicMutation` is, `GraphQL::Schema::Subscription`), or the schema does not
|
|
80
|
+
build. graphql-ruby never sees these three keys.
|
|
81
|
+
|
|
82
|
+
`rails g lighthouse:install` writes the schema class with them. Without them,
|
|
83
|
+
Lighthouse requires only the graphql-ruby class (`GraphQL::Schema::Resolver`, …),
|
|
84
|
+
and a `Subscription` field with no class resolves with a built-in subscription that
|
|
85
|
+
includes the concern.
|
|
86
|
+
|
|
87
|
+
## Resolvers and mutations: graphql-ruby's contract
|
|
88
|
+
|
|
89
|
+
A resolver or mutation class is a plain graphql-ruby
|
|
21
90
|
[resolver](https://graphql-ruby.org/fields/resolvers.html) or
|
|
22
91
|
[mutation](https://graphql-ruby.org/mutations/mutation_classes.html), so everything
|
|
23
92
|
you know from graphql-ruby applies:
|
|
@@ -40,9 +109,9 @@ type Mutation {
|
|
|
40
109
|
```
|
|
41
110
|
|
|
42
111
|
```ruby
|
|
43
|
-
# app/graphql/
|
|
44
|
-
module
|
|
45
|
-
class LatestPosts <
|
|
112
|
+
# app/graphql/resolvers/latest_posts.rb
|
|
113
|
+
module Resolvers
|
|
114
|
+
class LatestPosts < BaseResolver
|
|
46
115
|
def resolve(limit: 10)
|
|
47
116
|
Post.order(created_at: :desc).limit(limit)
|
|
48
117
|
end
|
|
@@ -51,7 +120,7 @@ end
|
|
|
51
120
|
|
|
52
121
|
# app/graphql/mutations/publish_post.rb
|
|
53
122
|
module Mutations
|
|
54
|
-
class PublishPost <
|
|
123
|
+
class PublishPost < BaseMutation
|
|
55
124
|
def authorized?(id:)
|
|
56
125
|
context[:current_user]&.admin? || raise(GraphQL::ExecutionError, 'Admins only.')
|
|
57
126
|
end
|
|
@@ -68,7 +137,7 @@ end
|
|
|
68
137
|
A field on `Query` or `Mutation` with **no resolving directive** resolves to the
|
|
69
138
|
class named after it, as in PHP Lighthouse:
|
|
70
139
|
|
|
71
|
-
- `Query.latestPosts` resolves to `
|
|
140
|
+
- `Query.latestPosts` resolves to `Resolvers::LatestPosts`;
|
|
72
141
|
- `Mutation.publishPost` resolves to `Mutations::PublishPost`.
|
|
73
142
|
|
|
74
143
|
The class is looked up on each call, so code reloading works. Classes found by
|
|
@@ -78,8 +147,8 @@ field with no class is reported through [`on_error`](configuration.md#on_error):
|
|
|
78
147
|
it raises in test and logs elsewhere. Lighthouse never falls back to a top-level
|
|
79
148
|
constant, so a `post` field is never mistaken for the `Post` model.
|
|
80
149
|
|
|
81
|
-
graphql-ruby's `RelayClassicMutation` (what `rails g graphql:install`
|
|
82
|
-
`Mutations::BaseMutation` from) works too, as long as the SDL field takes the
|
|
150
|
+
graphql-ruby's `RelayClassicMutation` (what `rails g graphql:install` makes
|
|
151
|
+
`Mutations::BaseMutation` inherit from) works too, as long as the SDL field takes the
|
|
83
152
|
`input:` argument it expects.
|
|
84
153
|
|
|
85
154
|
Wrapping directives (`@guard`, the `@can*` family, `@validator`, `@cache`,
|
|
@@ -95,12 +164,12 @@ type Mutation {
|
|
|
95
164
|
### Naming a class explicitly
|
|
96
165
|
|
|
97
166
|
`@field(resolver:)` points a field at a class by name. On `Query` and `Mutation`
|
|
98
|
-
the name is looked up in the
|
|
167
|
+
the name is looked up in the resolvers and mutations namespaces; on any other type,
|
|
99
168
|
in [`resolver_namespaces`](configuration.md#resolver_namespaces).
|
|
100
169
|
|
|
101
170
|
```graphql
|
|
102
171
|
type Query {
|
|
103
|
-
feed: [Post!]! @field(resolver: "Feeds::Home") #
|
|
172
|
+
feed: [Post!]! @field(resolver: "Feeds::Home") # Resolvers::Feeds::Home
|
|
104
173
|
}
|
|
105
174
|
```
|
|
106
175
|
|
|
@@ -166,7 +235,7 @@ end
|
|
|
166
235
|
## Generators
|
|
167
236
|
|
|
168
237
|
```shell
|
|
169
|
-
bin/rails generate lighthouse:query LatestPosts # app/graphql/
|
|
238
|
+
bin/rails generate lighthouse:query LatestPosts # app/graphql/resolvers/latest_posts.rb
|
|
170
239
|
bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/publish_post.rb
|
|
171
240
|
bin/rails generate lighthouse:subscription PostUpdated # app/graphql/subscriptions/post_updated.rb
|
|
172
241
|
bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
|
|
@@ -174,8 +243,10 @@ bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators
|
|
|
174
243
|
```
|
|
175
244
|
|
|
176
245
|
Each one writes the class with its contract, into the first namespace configured
|
|
177
|
-
for its kind (or `--namespace=…`).
|
|
178
|
-
|
|
246
|
+
for its kind (or `--namespace=…`). A resolver, mutation or subscription inherits
|
|
247
|
+
from the namespace's base class, which is written first when it is missing.
|
|
248
|
+
Namespaced names nest modules: `lighthouse:mutation Blog::Publish` writes
|
|
249
|
+
`Mutations::Blog::Publish < BaseMutation`. These
|
|
179
250
|
generators do not collide with graphql-ruby's `graphql:*` generators. A
|
|
180
251
|
`lighthouse:mutation` and a `graphql:mutation` of the same name target the same
|
|
181
252
|
file, and Rails asks before overwriting it.
|
|
@@ -184,8 +255,11 @@ file, and Rails asks before overwriting it.
|
|
|
184
255
|
|
|
185
256
|
```ruby
|
|
186
257
|
Lighthouse.configure do |config|
|
|
187
|
-
config.namespaces = { queries: ['Api::
|
|
258
|
+
config.namespaces = { queries: ['Api::Resolvers'], mutations: ['Api::Mutations'] }
|
|
188
259
|
end
|
|
189
260
|
```
|
|
190
261
|
|
|
191
262
|
Kinds you leave out keep their defaults. Each kind takes a list, tried in order.
|
|
263
|
+
The `queries` kind is the classes of `Query` fields. Before 0.4.0 it defaulted to
|
|
264
|
+
`Queries`; an app keeping its classes in `app/graphql/queries` sets
|
|
265
|
+
`config.namespaces = { queries: ['Queries'] }`.
|