lighthouse-graphql 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -1
  3. data/README.md +22 -3
  4. data/docs/best-practices.md +16 -6
  5. data/docs/caching.md +1 -1
  6. data/docs/configuration.md +74 -5
  7. data/docs/custom-directives.md +11 -1
  8. data/docs/directives.md +333 -2
  9. data/docs/events.md +203 -0
  10. data/docs/federation.md +3 -2
  11. data/docs/getting-started.md +90 -23
  12. data/docs/motivation.md +3 -2
  13. data/docs/mutations.md +15 -0
  14. data/docs/resolvers.md +95 -19
  15. data/docs/security.md +210 -0
  16. data/docs/subscriptions.md +446 -0
  17. data/lib/generators/lighthouse/base_classes.rb +87 -0
  18. data/lib/generators/lighthouse/class_generator.rb +19 -8
  19. data/lib/generators/lighthouse/install/install_generator.rb +23 -6
  20. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +7 -45
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +34 -5
  22. data/lib/generators/lighthouse/install/templates/schema.rb.tt +14 -2
  23. data/lib/generators/lighthouse/mutation_generator.rb +5 -4
  24. data/lib/generators/lighthouse/query_generator.rb +5 -4
  25. data/lib/generators/lighthouse/subscription_generator.rb +100 -0
  26. data/lib/lighthouse/broadcast_job.rb +14 -0
  27. data/lib/lighthouse/broadcasting.rb +143 -0
  28. data/lib/lighthouse/cache_control.rb +5 -2
  29. data/lib/lighthouse/complexity.rb +174 -0
  30. data/lib/lighthouse/complexity_analyzer.rb +56 -0
  31. data/lib/lighthouse/config.rb +56 -14
  32. data/lib/lighthouse/controller.rb +131 -0
  33. data/lib/lighthouse/directive_resolvers/broadcast_resolver.rb +31 -0
  34. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +14 -4
  35. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +134 -0
  36. data/lib/lighthouse/directive_resolvers/event_resolver.rb +37 -0
  37. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +28 -13
  38. data/lib/lighthouse/directive_resolvers/soft_delete_resolvers.rb +66 -0
  39. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +76 -0
  40. data/lib/lighthouse/directives/arguments/soft_deletes.rb +112 -0
  41. data/lib/lighthouse/directives/registry.rb +15 -6
  42. data/lib/lighthouse/directives/soft_deletes_directive.rb +123 -0
  43. data/lib/lighthouse/events/execution_trace.rb +52 -0
  44. data/lib/lighthouse/events/lifecycle.rb +148 -0
  45. data/lib/lighthouse/events/schema_build.rb +44 -0
  46. data/lib/lighthouse/events.rb +76 -0
  47. data/lib/lighthouse/feature_directive.rb +137 -0
  48. data/lib/lighthouse/field_resolver_stack.rb +28 -7
  49. data/lib/lighthouse/graphql/version.rb +1 -1
  50. data/lib/lighthouse/namespaces.rb +1 -0
  51. data/lib/lighthouse/persisted_queries/batch.rb +34 -0
  52. data/lib/lighthouse/persisted_queries.rb +17 -4
  53. data/lib/lighthouse/rb_lighthouse.rb +23 -0
  54. data/lib/lighthouse/resolver_bases.rb +61 -0
  55. data/lib/lighthouse/schema_cache.rb +12 -5
  56. data/lib/lighthouse/schema_factory.rb +0 -1
  57. data/lib/lighthouse/schema_generator.rb +42 -9
  58. data/lib/lighthouse/sdl_loader.rb +0 -2
  59. data/lib/lighthouse/subscription.rb +120 -0
  60. data/lib/lighthouse/support/model_resolver.rb +1 -1
  61. data/lib/lighthouse/support/soft_deletes.rb +164 -0
  62. data/lib/lighthouse/tasks/lighthouse.rake +10 -0
  63. data/sig/lighthouse/controller.rbs +30 -0
  64. data/sig/lighthouse/events.rbs +122 -0
  65. data/sig/lighthouse/graphql.rbs +62 -0
  66. data/sig/lighthouse.rbs +52 -1
  67. metadata +29 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 844e3fc4942a637b4353427932e031c210ad52d4d425161a58f45e2e36f1d931
4
- data.tar.gz: 9b0b05109d1fcac2f330b7266fe91eb502d521dabce399adfd98828137119468
3
+ metadata.gz: a15bb4d92716a08d5d248c5155b9cf336407a7988fed6e00c825b07bea4a206b
4
+ data.tar.gz: 421fca84187c5030f38c2cdd07ed26f9909747944f788cfd9662e67b6e322e2d
5
5
  SHA512:
6
- metadata.gz: ef2d22dd33355e36e3b40725cea55ebf5923a06ea2c94660228d8cc6b3a6e222bd7da6f42f796d52275a2130c1ebd9e54844f85febf31d66a7a4c759d310723e
7
- data.tar.gz: 7334b3e2b4ecb745fea9a4f9c08860d8e5c6245654323b0161a0b97c367d8ea022a3b23f7bd7e5bbd8c4e8614939b16e82fa737e9e133e4c5cba555199088c1d
6
+ metadata.gz: 388112925823baa63d0a7a4436bb1df4511f4a4c13b01001dd72b351156b78982ca4eab136a131c9c25b997915bff26099fa2a485bc4df74bfef390138e37c3a
7
+ data.tar.gz: e2f90e9654cfcdc2d26e084f68476ac59f56eb708683b3b4817cfc41006843363feb0f9a08b838bdcdd888395bc8f13ae29394d28b0bc37566c26dad63657cac
data/CHANGELOG.md CHANGED
@@ -6,6 +6,102 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-10-17
10
+
11
+ ### Added
12
+
13
+ - `@complexity` sets what a field costs in graphql-ruby's query complexity analysis, so
14
+ `AppSchema.max_complexity(n)` refuses expensive queries before they run. A bare `@complexity`
15
+ costs `1 + child complexity × first`. `@complexity(resolver: "Name")` names a
16
+ `GraphQL::Analysis::Analyzer` subclass in the new `analyzers` namespace (`Analyzers::Name`),
17
+ which Lighthouse installs on the schema. Including `Lighthouse::GraphQL::ComplexityAnalyzer`
18
+ gives the analyzer a `complexity` (a number, or a proc of `context, args, child_complexity`,
19
+ as graphql-ruby's) that is the cost of the fields naming it.
20
+ - A root field resolved by a `GraphQL::Schema::Resolver` class, by convention or through `@field`,
21
+ takes the `complexity` that class declares, as graphql-ruby's `field ..., resolver:` does.
22
+ - `BuildExtensionsResponse#query_complexity` holds the query's complexity score while something
23
+ listens to the event. It used to be `nil`.
24
+ - `docs/security.md`: query complexity, depth and introspection limits.
25
+ - graphql-ruby's base classes: `rails g lighthouse:install` writes `Resolvers::BaseResolver`,
26
+ `Mutations::BaseMutation` and `Subscriptions::BaseSubscription` (which includes
27
+ `Lighthouse::GraphQL::Subscription`), and `lighthouse:query`, `:mutation` and `:subscription`
28
+ write the one they need when it is missing. Generated classes inherit from them. An existing
29
+ base, such as the ones graphql-ruby's `graphql:install` writes, is never replaced.
30
+ - `get_schema`'s `base_types` takes `resolver:`, `mutation:` and `subscription:` beside
31
+ graphql-ruby's own keys. Every class a `Query`, `Mutation` or `Subscription` field resolves to
32
+ must then inherit from its kind's base (`Lighthouse::DefinitionError` otherwise), and a
33
+ `Subscription` field with no class of its own resolves with the subscription base. The
34
+ generated schema class names the three.
35
+
36
+ ### Changed
37
+
38
+ - The classes of `Query` fields are looked up in `Resolvers` (`app/graphql/resolvers`), graphql-ruby's
39
+ convention, instead of `Queries`: `Query.latestPosts` resolves to `Resolvers::LatestPosts`, and
40
+ `lighthouse:query` writes there. To keep `app/graphql/queries`, set
41
+ `config.namespaces = { queries: ['Queries'] }`.
42
+ - `lighthouse:subscription` no longer takes `--lighthouse`. The class inherits from
43
+ `Subscriptions::BaseSubscription`, which includes the concern, and gets its hooks; under a base
44
+ without the concern, it gets graphql-ruby's `authorized?` and `update`.
45
+
46
+ - Paginated fields (`@paginate`, and relationship directives given `type:`) cost
47
+ `1 + child complexity × first` in the complexity score. A page of 1,000 items now scores
48
+ 1,000 times one item's cost; before, a paginator scored the same whatever `first` was.
49
+ A schema that already sets `max_complexity` may need a higher limit.
50
+
51
+ ## [0.3.0] - 2026-10-17
52
+
53
+ ### Added
54
+
55
+ - `Lighthouse::GraphQL::Controller`, a Rails controller concern: `include` it, name the schema
56
+ with `lighthouse_schema AppSchema`, and its `execute` action runs the operation through
57
+ persisted queries, batches a JSON array body, reads the variables, sets the Cache-Control
58
+ header and renders the result. Its private hooks (`graphql_context`, `graphql_variables`,
59
+ `graphql_schema`) adjust it, and `lighthouse_request { }` gives an action of your own the
60
+ request events. The generated `GraphqlController` and the example app now use it.
61
+ - Events, as PHP Lighthouse dispatches them (`docs/events.md`): `Lighthouse::Events.listen`
62
+ for `StartRequest`, `StartOperationOrOperations`, `BuildSchemaString` (its listeners return SDL to add),
63
+ `RegisterDirectiveNamespaces` (its listeners return namespaces of directive classes to register), `ManipulateAST`,
64
+ `StartExecution`, `BuildExtensionsResponse` (its listeners return an `ExtensionsResponse` for the result's
65
+ `extensions`), `ManipulateResult`, `EndExecution`, `EndOperationOrOperations`, `EndRequest`
66
+ and `ValidateSchema`. Each one is also instrumented through `ActiveSupport::Notifications`
67
+ as `<event>.lighthouse`.
68
+ - `@event(dispatch:)` dispatches an event class, constructed with the field's resolved value,
69
+ once the field resolves. Event classes are looked up in `Config.namespaces[:events]` (`Events`).
70
+ - `PersistedQueries.multiplex` runs a batch of operations.
71
+ - `bin/rails lighthouse:validate_schema` dispatches `ValidateSchema` and builds every type.
72
+ - `@feature(name:, when:)` shows a field or object type only while a feature flag is in that
73
+ state, checked per query through graphql-ruby's visibility. It uses Flipper by default;
74
+ `Config.feature_checker` and `Config.feature_actor` change that.
75
+ - `@convertEmptyStringsToNull` on an argument, input field or field, or on every field with
76
+ `Config.convert_empty_strings_to_null`, turns incoming `""` into `null`.
77
+ - Soft deletes: `@softDeletes` adds a `trashed: Trashed @trashed` argument (`ONLY`, `WITH`,
78
+ `WITHOUT`), and `@restore` and `@forceDelete` restore or permanently delete the soft-deleted models
79
+ their filters select. They work with paranoia, discard or a plain `deleted_at` column,
80
+ detected per model, or with an adapter set in `Config.soft_deletes`.
81
+ - `@withoutGlobalScopes(names:)` lifts a model's default scope conditions when the client passes
82
+ `true`. Each name is a `without_<name>` scope or a column.
83
+ - Subscriptions (`docs/subscriptions.md`), on graphql-ruby's subscriptions: `Subscription`
84
+ fields resolve to plain `GraphQL::Schema::Subscription` classes in `Subscriptions::` by convention,
85
+ or to the class `@subscription(class:)` names, or work with no class at all.
86
+ `@broadcast(subscription:, shouldQueue:)` triggers a subscription once a mutation succeeds,
87
+ inline or in an ActiveJob (`Config.queue_broadcasts`, `Config.broadcast_queue`), with the class's
88
+ `self.trigger_arguments(root, context)` and `trigger_scope`; a plain class with required arguments
89
+ and no `trigger_arguments` (which could never reach a subscriber) is reported as a
90
+ `Lighthouse::DefinitionError`. The optional concern `Lighthouse::GraphQL::Subscription` adds PHP
91
+ Lighthouse's `authorize?`, `filter?` and `resolve_payload` hooks. `rails generate lighthouse:subscription`
92
+ writes a `GraphQL::Schema::Subscription` (`--lighthouse` to include the concern). The example app
93
+ has real ActionCable subscriptions (`GraphqlChannel`) with channel and end-to-end delivery specs.
94
+ - The initializer `rails generate lighthouse:install` writes lists the new settings, and
95
+ `docs/custom-directives.md` shows registering a namespace of directives through
96
+ `RegisterDirectiveNamespaces`.
97
+
98
+ ### Fixed
99
+
100
+ - Schemas using `@cacheControl` (or `@feature`) build on graphql 2.5 again. Their arguments
101
+ with a default are now nullable (`maxAge: Int = 0`, not `Int! = 0`), because graphql 2.5 does not
102
+ fill in a directive argument's default and so refused every usage that left one out. CI now
103
+ also runs the suite on graphql 2.5.
104
+
9
105
  ## [0.2.1] - 2026-10-17
10
106
 
11
107
  ### Added
@@ -228,7 +324,9 @@ had been running inside a private monorepo as a path gem before being extracted.
228
324
  - Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
229
325
  `on_error`, and `DirectiveRegistry.register` for app-defined directives.
230
326
 
231
- [Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.1...HEAD
327
+ [Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.4.0...HEAD
328
+ [0.4.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.3.0...v0.4.0
329
+ [0.3.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.1...v0.3.0
232
330
  [0.2.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.0...v0.2.1
233
331
  [0.2.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.1...v0.2.0
234
332
  [0.1.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -41,6 +41,13 @@ and falls back to a sensible default reader for plain fields. You write Ruby onl
41
41
  for the genuinely custom parts (`@field(resolver: "...")`), and your schema stays
42
42
  the single source of truth. [Motivation](docs/motivation.md) explains the idea in depth.
43
43
 
44
+ The same directives reach the rest of an API, too:
45
+ - real-time updates: `Subscription` fields on graphql-ruby's subscriptions, triggered
46
+ by a mutation's `@broadcast`;
47
+ - soft deletes (`@softDeletes`, `@restore`, `@forceDelete`) and feature flags (`@feature`);
48
+ - request lifecycle [events](docs/events.md);
49
+ - a controller concern that serves the schema in a few lines.
50
+
44
51
  ### Inspirations
45
52
 
46
53
  - **[Lighthouse (PHP)](https://lighthouse-php.com)** — the directive vocabulary,
@@ -62,6 +69,8 @@ gem 'lighthouse-graphql'
62
69
 
63
70
  It depends on `graphql ~> 2.5` and `activesupport`. For federation you also need
64
71
  `apollo-federation` in your app (it is an optional, app-provided dependency).
72
+ On graphql 2.5, keep the `json` gem below 3: graphql 2.5 calls `JSON.generate`
73
+ with an option json 3 removed (graphql 2.6 no longer does).
65
74
 
66
75
  Then let the generator set the app up:
67
76
 
@@ -118,7 +127,14 @@ Prefer to wire it by hand? These are the steps the generator performs.
118
127
 
119
128
  AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
120
129
  sdl_folder: Rails.root.join('graphql').to_s,
121
- options: { base_types: { object: BaseObject } } # your graphql-ruby base classes
130
+ options: {
131
+ base_types: {
132
+ resolver: Resolvers::BaseResolver, # graphql-ruby's base classes, which
133
+ mutation: Mutations::BaseMutation, # rails g lighthouse:install writes
134
+ subscription: Subscriptions::BaseSubscription
135
+ # object: Types::BaseObject, ...
136
+ }
137
+ }
122
138
  )
123
139
  ```
124
140
 
@@ -134,7 +150,8 @@ including base types and configuration.
134
150
  ### Example app
135
151
 
136
152
  [`example/`](example) is a small blog API built with Lighthouse on Rails 8.1. It shows
137
- the schema, the generated `Queries::`/`Mutations::`/`Builders::`/`Validators::` classes,
153
+ the schema, the generated `Resolvers::`/`Mutations::`/`Subscriptions::`/`Builders::`/`Validators::` classes
154
+ under graphql-ruby's base classes,
138
155
  Pundit policies and the opt-in performance features working together, and its specs
139
156
  are the library's end-to-end suite. See [example/README.md](example/README.md).
140
157
 
@@ -152,11 +169,13 @@ or as Markdown in [`docs/`](docs):
152
169
  | [Directives reference](docs/directives.md) | Every built-in directive, grouped by purpose |
153
170
  | [Relationships](docs/relationships.md) | `@hasMany`, `@belongsTo`, `@belongsToMany`, `@hasOne`, the `@morph*` variants, batching and `type:` pagination |
154
171
  | [Filtering & ordering](docs/filtering-and-ordering.md) | `@eq`/`@where`/`@in`/`@like`, `@whereConditions`, `@orderBy` |
155
- | [Queries, mutations & builders](docs/resolvers.md) | `Queries::` / `Mutations::` classes (graphql-ruby's contract), builders, generators |
172
+ | [Resolvers, mutations & builders](docs/resolvers.md) | `Resolvers::` / `Mutations::` classes and their base classes (graphql-ruby's contract), builders, generators |
156
173
  | [Mutations](docs/mutations.md) | `@create`, `@update`, `@upsert`, `@delete` (and `*Many`), nested mutations on every kind of association, `@spread`, `@inject` |
157
174
  | [Validation](docs/validation.md) | Model validations, `@validator` with Dry::Validation contracts |
158
175
  | [Authentication & authorization](docs/authorization.md) | `@guard`, `@canFind` / `@canModel` / `@canQuery` / `@canResolved` / `@canRoot` (Pundit by default) |
159
176
  | [Performance at scale](docs/performance.md) | Opt-in schema cache, SDL reloading in development, persisted queries (APQ) and trusted-document allowlists |
177
+ | [Events](docs/events.md) | Request, schema-build and execution events, `@event`, the controller concern |
178
+ | [Subscriptions](docs/subscriptions.md) | `Subscription` fields, `@subscription`, `@broadcast`, filtering and queued broadcasts |
160
179
  | [Caching](docs/caching.md) | `@cache`, `@cacheKey`, `@clearCache` (Rails.cache), `@cacheControl` (HTTP) |
161
180
  | [Apollo Federation](docs/federation.md) | `@key`, `resolve_reference`, the `ReferenceResolver` registry |
162
181
  | [Custom directives](docs/custom-directives.md) | The contracts, the registry, writing your own directive |
@@ -44,12 +44,22 @@ Use `@guard` and the `@can*` directives on fields that require them. They wrap t
44
44
  resolver, so the check runs before any data is loaded (only `@canResolved`, which
45
45
  needs the result, runs after — keep it to fields that don't write).
46
46
 
47
- ## Keep queries, mutations and builders apart
48
-
49
- A custom `Query` field is a `GraphQL::Schema::Resolver` in `app/graphql/queries`,
50
- a `Mutation` field a `GraphQL::Schema::Mutation` in `app/graphql/mutations`, and a
51
- query refinement a builder in `app/graphql/builders`. They have different jobs and
52
- contracts; don't fold them into one "resolvers" folder. See [resolvers.md](resolvers.md).
47
+ ## Keep resolvers, mutations and builders apart
48
+
49
+ A custom `Query` field is a `Resolvers::BaseResolver` in `app/graphql/resolvers`, a
50
+ `Mutation` field a `Mutations::BaseMutation` in `app/graphql/mutations`, and a query
51
+ refinement a builder in `app/graphql/builders`. They have different jobs and
52
+ contracts; keep each kind in its own folder. See [resolvers.md](resolvers.md).
53
+
54
+ ## Share through the base classes
55
+
56
+ What every resolver, mutation or subscription needs (a concern, a helper,
57
+ graphql-ruby's `argument_class`) goes in `Resolvers::BaseResolver`,
58
+ `Mutations::BaseMutation` or `Subscriptions::BaseSubscription`, once, as in any
59
+ graphql-ruby app. Name them in the schema's `base_types` (`resolver:`, `mutation:`,
60
+ `subscription:`), and a class that forgets to inherit from its base is refused
61
+ instead of quietly missing what it shares. See
62
+ [resolvers.md](resolvers.md#base-classes).
53
63
 
54
64
  ## Validate in the model first
55
65
 
data/docs/caching.md CHANGED
@@ -99,7 +99,7 @@ Each cached read costs one `read_multi` for the tokens.
99
99
  ## `@cacheControl`
100
100
 
101
101
  ```graphql
102
- directive @cacheControl(maxAge: Int! = 0, scope: CacheControlScope! = PUBLIC, inheritMaxAge: Boolean! = false)
102
+ directive @cacheControl(maxAge: Int = 0, scope: CacheControlScope = PUBLIC, inheritMaxAge: Boolean = false)
103
103
  on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
104
104
  ```
105
105
 
@@ -10,8 +10,8 @@ require 'lighthouse-graphql'
10
10
 
11
11
  Lighthouse.configure do |config|
12
12
  config.sdl_folder = Rails.root.join('graphql').to_s
13
- config.namespaces = { queries: ['Queries'], mutations: ['Mutations'] }
14
- config.resolver_namespaces = ['App::Graphql', 'Resolvers']
13
+ config.namespaces = { queries: ['Resolvers'], mutations: ['Mutations'] }
14
+ config.resolver_namespaces = ['App::Graphql']
15
15
  config.on_error = ->(error, _context) { Rails.error.report(error) }
16
16
  end
17
17
  ```
@@ -31,13 +31,16 @@ different contracts, so they live apart ([resolvers.md](resolvers.md)):
31
31
 
32
32
  | Kind | Default | Holds |
33
33
  | --- | --- | --- |
34
- | `queries` | `['Queries']` | `GraphQL::Schema::Resolver` classes for `Query` fields |
35
- | `mutations` | `['Mutations']` | `GraphQL::Schema::Mutation` classes for `Mutation` fields |
34
+ | `queries` | `['Resolvers']` | `Resolvers::BaseResolver` classes (a `GraphQL::Schema::Resolver`) for `Query` fields. `['Queries']` before 0.4.0 |
35
+ | `mutations` | `['Mutations']` | `Mutations::BaseMutation` classes (a `GraphQL::Schema::Mutation`) for `Mutation` fields |
36
36
  | `builders` | `['Builders']` | `@builder` classes |
37
37
  | `validators` | `['Validators']` | `@validator` contracts |
38
+ | `events` | `['Events']` | Classes `@event(dispatch:)` names ([events.md](events.md)) |
39
+ | `subscriptions` | `['Subscriptions']` | `Subscriptions::BaseSubscription` classes (a `GraphQL::Schema::Subscription`; the base includes `Lighthouse::GraphQL::Subscription`) for `Subscription` fields, and `@subscription(class:)` ([subscriptions.md](subscriptions.md)) |
40
+ | `analyzers` | `['Analyzers']` | `GraphQL::Analysis::Analyzer` classes (optionally including `Lighthouse::GraphQL::ComplexityAnalyzer`) that `@complexity(resolver:)` names ([security.md](security.md)) |
38
41
 
39
42
  ```ruby
40
- config.namespaces = { queries: ['Api::Queries'] } # the other kinds keep their defaults
43
+ config.namespaces = { queries: ['Api::Resolvers'] } # the other kinds keep their defaults
41
44
  ```
42
45
 
43
46
  ### `resolver_namespaces`
@@ -100,17 +103,83 @@ config.persisted_queries_manifest = Rails.root.join('persisted-queries.json') #
100
103
  config.persisted_queries_only = Rails.env.production? # run nothing else
101
104
  ```
102
105
 
106
+ ### `convert_empty_strings_to_null`
107
+
108
+ Makes every field behave as if it carried
109
+ [`@convertEmptyStringsToNull`](directives.md#convertemptystringstonull): empty
110
+ strings sent for nullable inputs arrive as `nil`, non-null inputs unchanged.
111
+ Defaults to `false`. It is read when the schema is built, so set it in the
112
+ initializer.
113
+
114
+ ```ruby
115
+ config.convert_empty_strings_to_null = true
116
+ ```
117
+
118
+ ### `queue_broadcasts`, `broadcast_queue`
119
+
120
+ Whether [`@broadcast`](directives.md#broadcast) triggers its subscription in an
121
+ ActiveJob (`Lighthouse::GraphQL::BroadcastJob`, needs the `activejob` gem)
122
+ instead of inside the mutation's request, and the queue it goes to. A directive's
123
+ `shouldQueue:` overrides `queue_broadcasts`. Defaults: `false`, and `nil` (the
124
+ default queue of ActiveJob). See [subscriptions.md](subscriptions.md#queueing).
125
+
126
+ ```ruby
127
+ config.queue_broadcasts = true
128
+ config.broadcast_queue = :subscriptions
129
+ ```
130
+
103
131
  ### `transactional_mutations`
104
132
 
105
133
  Whether `@create` / `@update` / `@createMany` / `@updateMany` run in a database
106
134
  transaction. Defaults to `true`.
107
135
 
136
+ ### `soft_deletes`
137
+
138
+ What "trashed" means, for `@softDeletes`, `@trashed`, `@restore`, `@forceDelete`
139
+ and `@withoutGlobalScopes`. By default each model is inspected: paranoia
140
+ (`acts_as_paranoid`), then discard (`Discard::Model`), then a `deleted_at` column.
141
+ Set it for another column, or a scheme of your own:
142
+
143
+ ```ruby
144
+ # Another column, for models without a gem:
145
+ config.soft_deletes = Lighthouse::Support::SoftDeletes::Column.new(:archived_at)
146
+
147
+ # Any object answering these (relations in, relations out; records in, truthy out):
148
+ class SoftDeleteAdapter
149
+ def soft_deletes?(model) = model.include?(Archivable)
150
+ def only_trashed(relation) = relation.unscope(where: :archived_at).where.not(archived_at: nil)
151
+ def with_trashed(relation) = relation.unscope(where: :archived_at)
152
+ def without_trashed(relation) = relation.unscope(where: :archived_at).where(archived_at: nil)
153
+ def restore(record) = record.unarchive
154
+ def force_delete(record) = record.destroy
155
+ end
156
+ config.soft_deletes = SoftDeleteAdapter.new
157
+ ```
158
+
159
+ See [Soft deletes & global scopes](directives.md#soft-deletes-global-scopes).
160
+
108
161
  ### `authenticated_user`, `policy_user`, `authorizer`
109
162
 
110
163
  The authentication and authorization seams behind `@guard` and the `@can*`
111
164
  family. The authorizer is `->(user, record, ability, args = [])`, Pundit by
112
165
  default. See [authorization.md](authorization.md#configuration).
113
166
 
167
+ ### `feature_checker`, `feature_actor`
168
+
169
+ The seams behind [`@feature`](directives.md#feature). The checker is
170
+ `->(name, actor, ctx) { true/false }`, called with the feature's name as written
171
+ in the SDL; by default it asks Flipper, `Flipper.enabled?(name.to_sym, actor)`,
172
+ and raises a clear error when Flipper is not loaded. The actor is
173
+ `->(ctx) { actor }`; by default it is the authenticated user (a Flipper actor
174
+ answers `flipper_id`), and `nil` checks the feature globally.
175
+
176
+ ```ruby
177
+ Lighthouse.configure do |config|
178
+ config.feature_actor = ->(ctx) { ctx[:current_account] }
179
+ config.feature_checker = ->(name, account, _ctx) { account&.features&.include?(name) }
180
+ end
181
+ ```
182
+
114
183
  ## Extending the registry
115
184
 
116
185
  Register app-defined directives (see [custom-directives.md](custom-directives.md))
@@ -109,7 +109,17 @@ Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::StartsWith)
109
109
  Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::UpcaseDirective)
110
110
  ```
111
111
 
112
- It is now usable in your SDL — no other changes:
112
+ To register every directive in a namespace at once, as PHP Lighthouse locates
113
+ directives by namespace, listen to the
114
+ [`RegisterDirectiveNamespaces`](events.md) event. Each class there with a
115
+ `directive_name` that is a resolver or an argument builder gets registered when
116
+ the schema is built:
117
+
118
+ ```ruby
119
+ Lighthouse::Events.listen(Lighthouse::Events::RegisterDirectiveNamespaces) { 'App::Graphql::Directives' }
120
+ ```
121
+
122
+ Either way, it is now usable in your SDL — no other changes:
113
123
 
114
124
  ```graphql
115
125
  type Query {