lighthouse-graphql 0.2.1 → 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 +56 -1
- data/README.md +11 -0
- 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/mutations.md +15 -0
- data/docs/resolvers.md +3 -1
- data/docs/subscriptions.md +423 -0
- 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/events.rbs +122 -0
- data/sig/lighthouse/graphql.rbs +33 -0
- data/sig/lighthouse.rbs +52 -1
- metadata +24 -1
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/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
|
|
@@ -167,6 +168,7 @@ end
|
|
|
167
168
|
```shell
|
|
168
169
|
bin/rails generate lighthouse:query LatestPosts # app/graphql/queries/latest_posts.rb
|
|
169
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
|
|
170
172
|
bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
|
|
171
173
|
bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators/create_post_input_validator.rb
|
|
172
174
|
```
|