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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -1
  3. data/README.md +11 -0
  4. data/docs/caching.md +1 -1
  5. data/docs/configuration.md +68 -0
  6. data/docs/custom-directives.md +11 -1
  7. data/docs/directives.md +306 -0
  8. data/docs/events.md +201 -0
  9. data/docs/getting-started.md +38 -9
  10. data/docs/mutations.md +15 -0
  11. data/docs/resolvers.md +3 -1
  12. data/docs/subscriptions.md +423 -0
  13. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +7 -45
  14. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +31 -3
  15. data/lib/generators/lighthouse/subscription_generator.rb +95 -0
  16. data/lib/lighthouse/broadcast_job.rb +14 -0
  17. data/lib/lighthouse/broadcasting.rb +143 -0
  18. data/lib/lighthouse/cache_control.rb +5 -2
  19. data/lib/lighthouse/config.rb +53 -12
  20. data/lib/lighthouse/controller.rb +131 -0
  21. data/lib/lighthouse/directive_resolvers/broadcast_resolver.rb +31 -0
  22. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +134 -0
  23. data/lib/lighthouse/directive_resolvers/event_resolver.rb +37 -0
  24. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +28 -13
  25. data/lib/lighthouse/directive_resolvers/soft_delete_resolvers.rb +66 -0
  26. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +68 -0
  27. data/lib/lighthouse/directives/arguments/soft_deletes.rb +112 -0
  28. data/lib/lighthouse/directives/registry.rb +14 -6
  29. data/lib/lighthouse/directives/soft_deletes_directive.rb +123 -0
  30. data/lib/lighthouse/events/execution_trace.rb +51 -0
  31. data/lib/lighthouse/events/lifecycle.rb +146 -0
  32. data/lib/lighthouse/events/schema_build.rb +44 -0
  33. data/lib/lighthouse/events.rb +76 -0
  34. data/lib/lighthouse/feature_directive.rb +137 -0
  35. data/lib/lighthouse/field_resolver_stack.rb +22 -2
  36. data/lib/lighthouse/graphql/version.rb +1 -1
  37. data/lib/lighthouse/namespaces.rb +1 -0
  38. data/lib/lighthouse/persisted_queries/batch.rb +34 -0
  39. data/lib/lighthouse/persisted_queries.rb +17 -4
  40. data/lib/lighthouse/rb_lighthouse.rb +20 -0
  41. data/lib/lighthouse/schema_cache.rb +12 -5
  42. data/lib/lighthouse/schema_generator.rb +27 -2
  43. data/lib/lighthouse/subscription.rb +120 -0
  44. data/lib/lighthouse/support/soft_deletes.rb +164 -0
  45. data/lib/lighthouse/tasks/lighthouse.rake +10 -0
  46. data/sig/lighthouse/controller.rbs +30 -0
  47. data/sig/lighthouse/events.rbs +122 -0
  48. data/sig/lighthouse/graphql.rbs +33 -0
  49. data/sig/lighthouse.rbs +52 -1
  50. 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`).
@@ -119,22 +119,51 @@ end
119
119
 
120
120
  ## 4. Serve it
121
121
 
122
- A standard graphql-ruby controller:
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
- def execute
127
- result = AppSchema.execute(
128
- params[:query],
129
- variables: params[:variables],
130
- context: { current_user: current_user },
131
- operation_name: params[:operationName]
132
- )
133
- render json: result
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 three kinds of class, and they are not
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
  ```