lighthouse-graphql 0.2.0 → 0.3.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 +77 -1
- data/README.md +16 -1
- data/docs/caching.md +1 -1
- data/docs/configuration.md +68 -0
- data/docs/custom-directives.md +11 -1
- data/docs/directives.md +306 -0
- data/docs/events.md +201 -0
- data/docs/getting-started.md +38 -9
- data/docs/motivation.md +184 -0
- data/docs/mutations.md +15 -0
- data/docs/resolvers.md +13 -1
- data/docs/subscriptions.md +423 -0
- data/lib/generators/lighthouse/builder_generator.rb +1 -1
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +7 -45
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +31 -3
- data/lib/generators/lighthouse/subscription_generator.rb +95 -0
- data/lib/lighthouse/broadcast_job.rb +14 -0
- data/lib/lighthouse/broadcasting.rb +143 -0
- data/lib/lighthouse/cache_control.rb +5 -2
- data/lib/lighthouse/config.rb +53 -12
- data/lib/lighthouse/controller.rb +131 -0
- data/lib/lighthouse/directive_resolvers/broadcast_resolver.rb +31 -0
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +134 -0
- data/lib/lighthouse/directive_resolvers/event_resolver.rb +37 -0
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +28 -13
- data/lib/lighthouse/directive_resolvers/soft_delete_resolvers.rb +66 -0
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +68 -0
- data/lib/lighthouse/directives/arguments/soft_deletes.rb +112 -0
- data/lib/lighthouse/directives/registry.rb +14 -6
- data/lib/lighthouse/directives/soft_deletes_directive.rb +123 -0
- data/lib/lighthouse/events/execution_trace.rb +51 -0
- data/lib/lighthouse/events/lifecycle.rb +146 -0
- data/lib/lighthouse/events/schema_build.rb +44 -0
- data/lib/lighthouse/events.rb +76 -0
- data/lib/lighthouse/feature_directive.rb +137 -0
- data/lib/lighthouse/field_resolver_stack.rb +22 -2
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/persisted_queries/batch.rb +34 -0
- data/lib/lighthouse/persisted_queries.rb +17 -4
- data/lib/lighthouse/rb_lighthouse.rb +20 -0
- data/lib/lighthouse/schema_cache.rb +12 -5
- data/lib/lighthouse/schema_generator.rb +27 -2
- data/lib/lighthouse/subscription.rb +120 -0
- data/lib/lighthouse/support/soft_deletes.rb +164 -0
- data/lib/lighthouse/tasks/lighthouse.rake +10 -0
- data/sig/lighthouse/controller.rbs +30 -0
- data/sig/lighthouse/directives.rbs +158 -0
- data/sig/lighthouse/events.rbs +122 -0
- data/sig/lighthouse/graphql.rbs +111 -0
- data/sig/lighthouse.rbs +193 -0
- metadata +29 -2
data/docs/events.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Events
|
|
2
|
+
|
|
3
|
+
Lighthouse dispatches events as a request goes by and as the schema is built, so
|
|
4
|
+
an app can hook into them without patching either. They mirror
|
|
5
|
+
[PHP Lighthouse's events](https://lighthouse-php.com/master/api-reference/events.html),
|
|
6
|
+
adapted to Ruby: a listener is a block or anything that responds to `call(event)`.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# config/initializers/lighthouse.rb
|
|
10
|
+
Lighthouse::Events.listen(Lighthouse::Events::StartExecution) do |event|
|
|
11
|
+
Rails.logger.info("GraphQL #{event.operation_name} at #{event.moment}")
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Listening
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
listener = Lighthouse::Events.listen(Lighthouse::Events::EndExecution) { |event| Metrics.record(event.result) }
|
|
19
|
+
Lighthouse::Events.listen(Lighthouse::Events::EndRequest, AuditLog) # anything with #call
|
|
20
|
+
Lighthouse::Events.forget(listener) # stop listening
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Listeners run in registration order. A listener on an event class also hears
|
|
24
|
+
its subclasses, so one on `Lighthouse::Events::Event` hears everything.
|
|
25
|
+
- `Lighthouse::Events.dispatch(event)` returns the non-nil values the listeners
|
|
26
|
+
returned. Some events give them a meaning (below); for the others they are
|
|
27
|
+
ignored, so a listener's last expression does no harm.
|
|
28
|
+
- Register in an initializer. Specs can call `Lighthouse::Events.reset!` to drop
|
|
29
|
+
every listener.
|
|
30
|
+
|
|
31
|
+
### ActiveSupport::Notifications
|
|
32
|
+
|
|
33
|
+
Every dispatch is also instrumented as `<event name>.lighthouse`, with the event
|
|
34
|
+
as the `:event` payload, so Rails tooling observes them with no listener of ours:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
ActiveSupport::Notifications.subscribe('end_execution.lighthouse') do |*, payload|
|
|
38
|
+
Rails.logger.info(payload[:event].result['errors'])
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The name is the class name underscored: `start_request.lighthouse`,
|
|
43
|
+
`build_schema_string.lighthouse`, ...
|
|
44
|
+
|
|
45
|
+
## Request lifecycle
|
|
46
|
+
|
|
47
|
+
In the order they happen. Events with a `moment` carry the `Time` they were
|
|
48
|
+
created at (UTC).
|
|
49
|
+
|
|
50
|
+
| Event | When | Readers |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `StartRequest` | The controller action begins | `request`, `moment` |
|
|
53
|
+
| `StartOperationOrOperations` | The operation(s) were read, before they run | `operation_or_operations` |
|
|
54
|
+
| `StartExecution` | A query is about to run (once per operation of a batch) | `schema`, `query`, `variables`, `operation_name`, `context`, `moment` |
|
|
55
|
+
| `BuildExtensionsResponse` | A query ran | `result`, `query_complexity` |
|
|
56
|
+
| `ManipulateResult` | Before the result is returned | `result` (+ writer) |
|
|
57
|
+
| `EndExecution` | The result is final | `result`, `moment` |
|
|
58
|
+
| `EndOperationOrOperations` | The operation(s) ran | `result_or_results` |
|
|
59
|
+
| `EndRequest` | The response is rendered | `response`, `moment` |
|
|
60
|
+
|
|
61
|
+
`StartRequest` and `EndRequest` are dispatched by `execute`, the action of the
|
|
62
|
+
`Lighthouse::GraphQL::Controller` concern that `rails g lighthouse:install`
|
|
63
|
+
generates a controller with ([getting-started.md](getting-started.md)):
|
|
64
|
+
the first thing it does, and right after it renders. An action of your own gets
|
|
65
|
+
them by wrapping its body in `lighthouse_request`, which dispatches them once
|
|
66
|
+
however it nests:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
def search
|
|
70
|
+
lighthouse_request do
|
|
71
|
+
render json: Lighthouse::GraphQL::PersistedQueries.execute(AppSchema, query: params[:q])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Without the concern, dispatch them yourself:
|
|
77
|
+
`Lighthouse::Events.dispatch(Lighthouse::Events::StartRequest.new(request))`, and
|
|
78
|
+
`EndRequest.new(response)` after rendering.
|
|
79
|
+
|
|
80
|
+
`StartOperationOrOperations` and `EndOperationOrOperations` are dispatched by
|
|
81
|
+
`Lighthouse::GraphQL::PersistedQueries.execute`, which the generated controller
|
|
82
|
+
calls. The first carries a Hash (`query`, `variables`, `operation_name`,
|
|
83
|
+
`extensions`), as the request named them. For a batch, run the operations through
|
|
84
|
+
`PersistedQueries.multiplex(schema, operations, context: ...)` instead (the concern does for a JSON array body): it takes
|
|
85
|
+
an Array of such Hashes (each may also carry its own `context`), dispatches the
|
|
86
|
+
Arrays, and answers one result per operation. Operations that run straight on
|
|
87
|
+
`schema.execute` or `schema.multiplex` fire only the execution events below.
|
|
88
|
+
|
|
89
|
+
### Execution events
|
|
90
|
+
|
|
91
|
+
`StartExecution`, `BuildExtensionsResponse`, `ManipulateResult` and
|
|
92
|
+
`EndExecution` come from a graphql-ruby trace Lighthouse installs on every schema
|
|
93
|
+
it builds, so they fire however the query was started: the controller,
|
|
94
|
+
`schema.execute` in a job, or a spec. A multiplex fires them once per query.
|
|
95
|
+
With no listener, and no `ActiveSupport::Notifications` subscriber, they cost
|
|
96
|
+
one lookup per request.
|
|
97
|
+
|
|
98
|
+
`StartExecution#query` is the parsed `GraphQL::Language::Nodes::Document`.
|
|
99
|
+
|
|
100
|
+
**`BuildExtensionsResponse`**: return an `ExtensionsResponse` (or an Array of
|
|
101
|
+
them) to add to the result's `extensions`:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Lighthouse::Events.listen(Lighthouse::Events::BuildExtensionsResponse) do |event|
|
|
105
|
+
Lighthouse::Events::ExtensionsResponse.new('queries', Query.count_for(event.result))
|
|
106
|
+
end
|
|
107
|
+
# => { "data" => {...}, "extensions" => { "queries" => 3 } }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`query_complexity` is `nil`: Lighthouse does not compute it for you.
|
|
111
|
+
|
|
112
|
+
**`ManipulateResult`**: change `event.result` (the Hash with `data`, `errors` and
|
|
113
|
+
`extensions`) in place, or assign a new Hash to `event.result=`:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
Lighthouse::Events.listen(Lighthouse::Events::ManipulateResult) do |event|
|
|
117
|
+
event.result['extensions'] = (event.result['extensions'] || {}).merge('served_by' => Socket.gethostname)
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Both also fire for a query that fails validation; the result then holds `errors`
|
|
123
|
+
and no `data`.
|
|
124
|
+
|
|
125
|
+
## Schema build events
|
|
126
|
+
|
|
127
|
+
These fire while the schema is built, once per build. Listen from an initializer
|
|
128
|
+
that runs before the schema is first used (the schema constant autoloads when
|
|
129
|
+
something first refers to it).
|
|
130
|
+
|
|
131
|
+
**`RegisterDirectiveNamespaces`**: return a namespace (a `Module`, or its name)
|
|
132
|
+
or an Array of them. Every directive class under them (a constant that responds
|
|
133
|
+
to `directive_name`: a field resolver or an argument builder) is registered, as
|
|
134
|
+
if with `Lighthouse::DirectiveRegistry.register`:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
Lighthouse::Events.listen(Lighthouse::Events::RegisterDirectiveNamespaces) do
|
|
138
|
+
%w[App::Graphql::Directives MyEngine::Directives]
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A name that is no constant fails the build with `Lighthouse::DefinitionError`.
|
|
143
|
+
It fires before the directive definitions are injected, on every build,
|
|
144
|
+
schema cache or not.
|
|
145
|
+
|
|
146
|
+
**`BuildSchemaString`**: return SDL to append to the app's schema, before the
|
|
147
|
+
manipulation phase (so it can use any directive):
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
Lighthouse::Events.listen(Lighthouse::Events::BuildSchemaString) do |event|
|
|
151
|
+
'extend type Query { version: String @field(resolver: "Version") }'
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**`ManipulateAST`**: the document after manipulation, right before the schema
|
|
156
|
+
is built from it. graphql-ruby's nodes are immutable, so replace the document:
|
|
157
|
+
`event.document_ast = GraphQL.parse(...)`.
|
|
158
|
+
|
|
159
|
+
With [`schema_cache`](performance.md#schema-cache) on, these two fire only when the document
|
|
160
|
+
is actually built, on a cache miss, and the cache holds what they made. The cache
|
|
161
|
+
key does not cover them: after you change what a listener adds, run
|
|
162
|
+
`bin/rails lighthouse:clear_cache`.
|
|
163
|
+
|
|
164
|
+
## `ValidateSchema`
|
|
165
|
+
|
|
166
|
+
`bin/rails lighthouse:validate_schema` (`SCHEMA=AppSchema` names another schema)
|
|
167
|
+
dispatches `ValidateSchema` with the schema, then has graphql-ruby build and print
|
|
168
|
+
every type. A listener that raises, or a schema graphql-ruby rejects, aborts the
|
|
169
|
+
task with a message, so it can run in CI:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Lighthouse::Events.listen(Lighthouse::Events::ValidateSchema) do |event|
|
|
173
|
+
raise 'The schema has no mutations' unless event.schema.mutation
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Dispatching your own: `@event`
|
|
178
|
+
|
|
179
|
+
The [`@event`](directives.md#event) directive dispatches an event of yours once a
|
|
180
|
+
field has resolved:
|
|
181
|
+
|
|
182
|
+
```graphql
|
|
183
|
+
type Mutation {
|
|
184
|
+
publishPost(id: ID!): Post! @update @event(dispatch: "PostPublished")
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# app/events/post_published.rb
|
|
190
|
+
class PostPublished
|
|
191
|
+
attr_reader :post
|
|
192
|
+
|
|
193
|
+
def initialize(post) = @post = post
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
Lighthouse::Events.listen(PostPublished) { |event| Mailer.published(event.post).deliver_later }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Any class works as an event. Lighthouse looks the name up in
|
|
200
|
+
`Config.namespaces[:events]` (default `['Events']`, so `Events::PostPublished`,
|
|
201
|
+
then `::PostPublished`).
|
data/docs/getting-started.md
CHANGED
|
@@ -119,22 +119,51 @@ end
|
|
|
119
119
|
|
|
120
120
|
## 4. Serve it
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
`rails g lighthouse:install` writes the controller. It includes
|
|
123
|
+
`Lighthouse::GraphQL::Controller`, a concern that serves the schema:
|
|
123
124
|
|
|
124
125
|
```ruby
|
|
125
126
|
class GraphqlController < ApplicationController
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
include Lighthouse::GraphQL::Controller
|
|
128
|
+
|
|
129
|
+
lighthouse_schema AppSchema # or 'AppSchema', or a block: lighthouse_schema { AppSchema }
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# What @guard and the @can* directives read. Defaults to
|
|
134
|
+
# { current_user: current_user } when the controller has a current_user.
|
|
135
|
+
def graphql_context = { current_user: current_user }
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Route to it (`post '/graphql', to: 'graphql#execute'`, plus a `get` for
|
|
140
|
+
[persisted queries](performance.md#persisted-queries) if you use them). Its
|
|
141
|
+
`execute` action:
|
|
142
|
+
|
|
143
|
+
- dispatches the [`StartRequest` and `EndRequest` events](events.md);
|
|
144
|
+
- reads `variables` (a JSON string, as a GET carries it, or parameters);
|
|
145
|
+
- runs the operation through `PersistedQueries.execute`, so
|
|
146
|
+
[Automatic Persisted Queries](performance.md#persisted-queries) work once
|
|
147
|
+
`config.persisted_queries` is on, and a JSON array body runs as a batch;
|
|
148
|
+
- sets the `Cache-Control` header the selected fields'
|
|
149
|
+
[`@cacheControl`](caching.md) hints allow;
|
|
150
|
+
- renders the JSON; in development an error renders as JSON with its backtrace.
|
|
151
|
+
|
|
152
|
+
The hooks `graphql_context`, `graphql_schema` and `graphql_variables(param)` are
|
|
153
|
+
private methods you can override. A controller with an action of its own wraps it
|
|
154
|
+
in `lighthouse_request { ... }` to keep the request events:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
def search
|
|
158
|
+
lighthouse_request do
|
|
159
|
+
render json: Lighthouse::GraphQL::PersistedQueries.execute(AppSchema, query: params[:q], context: graphql_context)
|
|
134
160
|
end
|
|
135
161
|
end
|
|
136
162
|
```
|
|
137
163
|
|
|
164
|
+
Not on the concern? Plain graphql-ruby works too:
|
|
165
|
+
`render json: AppSchema.execute(params[:query], variables: params[:variables], context: { current_user: current_user })`.
|
|
166
|
+
|
|
138
167
|
## 5. Configure (optional)
|
|
139
168
|
|
|
140
169
|
Create an initializer to wire app-specific settings. This is also the single
|
data/docs/motivation.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Motivation
|
|
2
|
+
|
|
3
|
+
Lighthouse GraphQL Rails brings the ideas of [Lighthouse](https://lighthouse-php.com),
|
|
4
|
+
the GraphQL framework for Laravel, to Ruby on Rails. This page explains those ideas:
|
|
5
|
+
why the schema comes first, why behaviour lives in directives, and why that works so
|
|
6
|
+
well on top of an opinionated model layer like Active Record.
|
|
7
|
+
|
|
8
|
+
## Where it comes from
|
|
9
|
+
|
|
10
|
+
Lighthouse ([nuwave/lighthouse](https://github.com/nuwave/lighthouse)) changed how many
|
|
11
|
+
Laravel teams build GraphQL APIs. Instead of a resolver class for every field, you write
|
|
12
|
+
the schema in GraphQL's own language and attach **directives** that say what a field
|
|
13
|
+
does: `@paginate` it, follow a `@hasMany` relationship, `@create` a model, check a policy
|
|
14
|
+
with `@canFind`. Lighthouse reads those directives and generates efficient queries against
|
|
15
|
+
Eloquent, Laravel's ORM.
|
|
16
|
+
|
|
17
|
+
The idea does not depend on PHP. It depends on two things: a schema written first, and a
|
|
18
|
+
model layer with strong conventions. Rails has the second in Active Record, and
|
|
19
|
+
[graphql-ruby](https://graphql-ruby.org) gives Ruby a mature GraphQL engine to build the
|
|
20
|
+
first on. Lighthouse GraphQL Rails puts them together. It keeps Lighthouse's directive
|
|
21
|
+
names, arguments and error formats wherever they make sense, so its documentation, its
|
|
22
|
+
examples and the experience of Laravel teams carry over.
|
|
23
|
+
|
|
24
|
+
## The problem with writing every resolver
|
|
25
|
+
|
|
26
|
+
Most GraphQL fields in a Rails app do one of a handful of things: list records, find one,
|
|
27
|
+
paginate, follow an association, filter and sort, read an attribute, or create, update
|
|
28
|
+
and delete a record. Written by hand, each of these is a resolver. Each resolver is a
|
|
29
|
+
place where:
|
|
30
|
+
|
|
31
|
+
- **the API's shape hides in Ruby.** To know what `posts` accepts and returns, you read
|
|
32
|
+
a type class, its fields, their resolver methods and the scopes they call.
|
|
33
|
+
- **N+1 queries creep in.** Every association needs its own batch loader, and every
|
|
34
|
+
paginated association needs a separate count, page and cursor logic.
|
|
35
|
+
- **the same decisions are made again**, slightly differently each time: how pages are
|
|
36
|
+
numbered, what a cursor contains, how validation errors are reported, which filter
|
|
37
|
+
arguments are allowed.
|
|
38
|
+
|
|
39
|
+
With [graphql-ruby](https://graphql-ruby.org) alone, a list of published posts with their
|
|
40
|
+
author and comments, filterable by author, looks roughly like this:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class Types::PostType < Types::BaseObject
|
|
44
|
+
field :id, ID, null: false
|
|
45
|
+
field :title, String, null: false
|
|
46
|
+
field :author, Types::UserType, null: false
|
|
47
|
+
field :comments, Types::CommentType.connection_type, null: false
|
|
48
|
+
|
|
49
|
+
def author
|
|
50
|
+
dataloader.with(Sources::RecordById, User).load(object.author_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def comments
|
|
54
|
+
object.comments.order(:created_at) # one query per post
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Types::QueryType < Types::BaseObject
|
|
59
|
+
field :posts, Types::PostType.connection_type, null: false do
|
|
60
|
+
argument :author_id, ID, required: false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def posts(author_id: nil)
|
|
64
|
+
scope = Post.published.order(created_at: :desc)
|
|
65
|
+
author_id ? scope.where(author_id: author_id) : scope
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
…plus the `Sources::RecordById` batch loader, and more code to batch the comments. With
|
|
71
|
+
Lighthouse, it is the schema:
|
|
72
|
+
|
|
73
|
+
```graphql
|
|
74
|
+
type Query {
|
|
75
|
+
posts(authorId: ID @eq): [Post!]! @paginate(type: CONNECTION) @builder(class: "Published")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type Post {
|
|
79
|
+
id: ID!
|
|
80
|
+
title: String!
|
|
81
|
+
author: User! @belongsTo
|
|
82
|
+
comments: [Comment!]! @hasMany(type: CONNECTION, scopes: ["chronological"])
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `author` of every post loads in one query, and so does every post's page of
|
|
87
|
+
comments, with one more for their totals if the client asks for `pageInfo { total }`. This isn't a
|
|
88
|
+
special case: it is how every relationship directive works.
|
|
89
|
+
|
|
90
|
+
## Why the schema comes first
|
|
91
|
+
|
|
92
|
+
In a schema-first API, the `.graphql` files *are* the API's definition, not something
|
|
93
|
+
generated from Ruby classes:
|
|
94
|
+
|
|
95
|
+
- **It is a contract you can read.** Frontend developers, mobile developers and reviewers
|
|
96
|
+
read the same SDL that clients query. A pull request that changes the API shows the
|
|
97
|
+
change in the schema diff.
|
|
98
|
+
- **It is designed before it is implemented.** You can agree on the types and fields
|
|
99
|
+
first, as a team, and the server follows the design rather than leaking its internals.
|
|
100
|
+
- **Tools understand it directly.** IDE plugins, code generators and schema registries
|
|
101
|
+
read SDL. Lighthouse's `lighthouse:ide_helper` task adds its directives and generated
|
|
102
|
+
types so editors validate and autocomplete the schema.
|
|
103
|
+
- **It says what each field does.** `posts: [Post!]! @paginate @guard @cacheControl(maxAge: 60)`
|
|
104
|
+
tells a reader, without opening any Ruby, that the field is paginated, requires a
|
|
105
|
+
signed-in user and can be cached for a minute.
|
|
106
|
+
|
|
107
|
+
## Why directives
|
|
108
|
+
|
|
109
|
+
Directives are a small vocabulary that composes. Each one does one job, and a field
|
|
110
|
+
combines them:
|
|
111
|
+
|
|
112
|
+
```graphql
|
|
113
|
+
type Mutation {
|
|
114
|
+
updatePost(input: UpdatePostInput! @spread): Post!
|
|
115
|
+
@guard # require a user
|
|
116
|
+
@canFind(ability: "update", find: "input.id") # PostPolicy#update?
|
|
117
|
+
@update # save through Active Record, in a transaction
|
|
118
|
+
@clearCache(type: "Post", idSource: { argument: "input.id" })
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Because the behaviour is declared, Lighthouse controls the order it runs in
|
|
123
|
+
(authentication, then authorization, then validation, then the save, then cache
|
|
124
|
+
invalidation) and handles the details the same way everywhere: pagination types and
|
|
125
|
+
cursors, validation errors keyed by argument path, `Cache-Control` headers. That
|
|
126
|
+
consistency is what a hand-written resolver per field rarely achieves.
|
|
127
|
+
|
|
128
|
+
## Why it fits Active Record
|
|
129
|
+
|
|
130
|
+
A directive can only be this short if something else knows the details. In Lighthouse,
|
|
131
|
+
that is the model layer. Active Record is opinionated, and every convention it imposes is
|
|
132
|
+
information Lighthouse can use:
|
|
133
|
+
|
|
134
|
+
| Active Record already knows | So Lighthouse can |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| Associations, their foreign keys, `:through` and polymorphic links | Resolve `@hasMany`, `@belongsTo`, `@belongsToMany` and `@morph*` from the name alone, batch them, and paginate many parents in one query |
|
|
137
|
+
| Scopes | Narrow any query with `scopes: ["published"]` |
|
|
138
|
+
| Validations | Report a failed `@create` or `@update` by argument path, with the model's own messages and locale |
|
|
139
|
+
| Transactions and callbacks | Run nested mutations (create a post, attach tags, add comments) atomically, with your callbacks |
|
|
140
|
+
| Naming conventions (`Post`, `posts`, `author_id`) | Map `Post` to its model, `authorId` to `author_id`, and a field to its association, with no configuration |
|
|
141
|
+
|
|
142
|
+
The schema says **what** the API offers; the models already say **how** the data
|
|
143
|
+
behaves. Nothing is described twice, and your domain logic stays where Rails puts it,
|
|
144
|
+
in models, scopes, validations and policies, instead of moving into GraphQL resolvers.
|
|
145
|
+
That is the same reason Lighthouse works so well on Eloquent: both are opinionated
|
|
146
|
+
Active Record ORMs, and conventions are what make a declarative layer on top possible.
|
|
147
|
+
|
|
148
|
+
## Ruby where there is real logic
|
|
149
|
+
|
|
150
|
+
Schema-first doesn't mean schema-only. When a field does something no directive
|
|
151
|
+
describes, write Ruby, using graphql-ruby's own classes:
|
|
152
|
+
|
|
153
|
+
- A `Query` or `Mutation` field without a resolving directive resolves by convention to
|
|
154
|
+
`Queries::FieldName` (a `GraphQL::Schema::Resolver`) or `Mutations::FieldName` (a
|
|
155
|
+
`GraphQL::Schema::Mutation`). See [Queries, Mutations and Builders](resolvers.md).
|
|
156
|
+
- `@builder` classes adjust a query; `@validator` contracts check arguments.
|
|
157
|
+
- Your own directives plug in through the registry, with the same contracts as the
|
|
158
|
+
built-in ones. See [Custom Directives](custom-directives.md).
|
|
159
|
+
|
|
160
|
+
Lighthouse is built on graphql-ruby, not instead of it: its Dataloader, its execution
|
|
161
|
+
engine and its classes are all there when you need them.
|
|
162
|
+
|
|
163
|
+
## From small apps to large ones
|
|
164
|
+
|
|
165
|
+
The same schema serves a side project and a busy API. When it grows, opt-in features keep
|
|
166
|
+
it fast without changing the schema: a cached schema build for quick boots, Apollo's
|
|
167
|
+
persisted queries for CDN caching, and an allowlist of trusted operations. They are all
|
|
168
|
+
off by default. See [Performance at scale](performance.md).
|
|
169
|
+
|
|
170
|
+
## When something else fits better
|
|
171
|
+
|
|
172
|
+
Lighthouse is at its best when the API is close to your models. Consider plain
|
|
173
|
+
graphql-ruby, or using Lighthouse only for part of the schema, when:
|
|
174
|
+
|
|
175
|
+
- the API is mostly computed, aggregated or fetched from other services, rather than read
|
|
176
|
+
from and written to your database;
|
|
177
|
+
- your team prefers to define the schema in Ruby and generate the SDL from it
|
|
178
|
+
(code-first);
|
|
179
|
+
- types differ so much from your tables that most fields would need their own resolver.
|
|
180
|
+
|
|
181
|
+
Both approaches run on graphql-ruby, and a Lighthouse schema can call graphql-ruby
|
|
182
|
+
classes anywhere, so you can start with directives and write Ruby where it pays off.
|
|
183
|
+
|
|
184
|
+
Ready to try it? Continue with [Getting Started](getting-started.md).
|
data/docs/mutations.md
CHANGED
|
@@ -96,6 +96,21 @@ Without a filter argument, it refuses to run, since it would delete every row:
|
|
|
96
96
|
abort the destroy is reported next to the data:
|
|
97
97
|
`Could not modify model Post with ID 1.`
|
|
98
98
|
|
|
99
|
+
### `@forceDelete` and `@restore`
|
|
100
|
+
|
|
101
|
+
`@forceDelete` permanently removes soft deleted models and `@restore` un-deletes
|
|
102
|
+
them. They work very much like `@delete` (required filter arguments, the models
|
|
103
|
+
returned, `model:` and `scopes:`), but also find the models the soft-delete
|
|
104
|
+
strategy hides. See [Soft deletes & global
|
|
105
|
+
scopes](directives.md#soft-deletes-global-scopes).
|
|
106
|
+
|
|
107
|
+
```graphql
|
|
108
|
+
type Mutation {
|
|
109
|
+
forceDeletePost(id: ID! @whereKey): Post @forceDelete
|
|
110
|
+
restorePost(id: ID! @whereKey): Post @restore
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
99
114
|
## Validation errors
|
|
100
115
|
|
|
101
116
|
When a model does not save, the mutation fails with PHP Lighthouse's error
|
data/docs/resolvers.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
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
|
|
5
|
-
up a class by name. There are
|
|
5
|
+
up a class by name. There are several kinds of class, and they are not
|
|
6
6
|
interchangeable. Each kind has its own contract and its own folder:
|
|
7
7
|
|
|
8
8
|
| Kind | Folder / namespace | Contract | Used by |
|
|
9
9
|
| --- | --- | --- | --- |
|
|
10
10
|
| Query | `app/graphql/queries` · `Queries::` | `GraphQL::Schema::Resolver` | `Query` fields |
|
|
11
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)) |
|
|
12
13
|
| Builder | `app/graphql/builders` · `Builders::` | `call(relation, args, context)` → relation | `@builder` |
|
|
13
14
|
|
|
14
15
|
Validators (`app/graphql/validators`) are the fourth kind. They are covered in
|
|
@@ -152,11 +153,22 @@ are looked up in the builders namespace only. Raising a `GraphQL::ExecutionError
|
|
|
152
153
|
from a builder (e.g. a tenancy check) always reaches the client; it never falls
|
|
153
154
|
back to the unscoped relation.
|
|
154
155
|
|
|
156
|
+
The gem ships RBS signatures in `sig/`, so with Steep (or anything that reads
|
|
157
|
+
`rbs collection`) a builder can be checked against `Lighthouse::_Builder`:
|
|
158
|
+
|
|
159
|
+
```rbs
|
|
160
|
+
# sig/builders/published.rbs
|
|
161
|
+
class Builders::Published
|
|
162
|
+
extend Lighthouse::_Builder
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
155
166
|
## Generators
|
|
156
167
|
|
|
157
168
|
```shell
|
|
158
169
|
bin/rails generate lighthouse:query LatestPosts # app/graphql/queries/latest_posts.rb
|
|
159
170
|
bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/publish_post.rb
|
|
171
|
+
bin/rails generate lighthouse:subscription PostUpdated # app/graphql/subscriptions/post_updated.rb
|
|
160
172
|
bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
|
|
161
173
|
bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators/create_post_input_validator.rb
|
|
162
174
|
```
|