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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/README.md +16 -1
  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/motivation.md +184 -0
  11. data/docs/mutations.md +15 -0
  12. data/docs/resolvers.md +13 -1
  13. data/docs/subscriptions.md +423 -0
  14. data/lib/generators/lighthouse/builder_generator.rb +1 -1
  15. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +7 -45
  16. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +31 -3
  17. data/lib/generators/lighthouse/subscription_generator.rb +95 -0
  18. data/lib/lighthouse/broadcast_job.rb +14 -0
  19. data/lib/lighthouse/broadcasting.rb +143 -0
  20. data/lib/lighthouse/cache_control.rb +5 -2
  21. data/lib/lighthouse/config.rb +53 -12
  22. data/lib/lighthouse/controller.rb +131 -0
  23. data/lib/lighthouse/directive_resolvers/broadcast_resolver.rb +31 -0
  24. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +134 -0
  25. data/lib/lighthouse/directive_resolvers/event_resolver.rb +37 -0
  26. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +28 -13
  27. data/lib/lighthouse/directive_resolvers/soft_delete_resolvers.rb +66 -0
  28. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +68 -0
  29. data/lib/lighthouse/directives/arguments/soft_deletes.rb +112 -0
  30. data/lib/lighthouse/directives/registry.rb +14 -6
  31. data/lib/lighthouse/directives/soft_deletes_directive.rb +123 -0
  32. data/lib/lighthouse/events/execution_trace.rb +51 -0
  33. data/lib/lighthouse/events/lifecycle.rb +146 -0
  34. data/lib/lighthouse/events/schema_build.rb +44 -0
  35. data/lib/lighthouse/events.rb +76 -0
  36. data/lib/lighthouse/feature_directive.rb +137 -0
  37. data/lib/lighthouse/field_resolver_stack.rb +22 -2
  38. data/lib/lighthouse/graphql/version.rb +1 -1
  39. data/lib/lighthouse/namespaces.rb +1 -0
  40. data/lib/lighthouse/persisted_queries/batch.rb +34 -0
  41. data/lib/lighthouse/persisted_queries.rb +17 -4
  42. data/lib/lighthouse/rb_lighthouse.rb +20 -0
  43. data/lib/lighthouse/schema_cache.rb +12 -5
  44. data/lib/lighthouse/schema_generator.rb +27 -2
  45. data/lib/lighthouse/subscription.rb +120 -0
  46. data/lib/lighthouse/support/soft_deletes.rb +164 -0
  47. data/lib/lighthouse/tasks/lighthouse.rake +10 -0
  48. data/sig/lighthouse/controller.rbs +30 -0
  49. data/sig/lighthouse/directives.rbs +158 -0
  50. data/sig/lighthouse/events.rbs +122 -0
  51. data/sig/lighthouse/graphql.rbs +111 -0
  52. data/sig/lighthouse.rbs +193 -0
  53. 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`).
@@ -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
@@ -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 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
@@ -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
  ```