lighthouse-graphql 0.1.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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +134 -0
  5. data/docs/authorization.md +109 -0
  6. data/docs/best-practices.md +75 -0
  7. data/docs/configuration.md +70 -0
  8. data/docs/custom-directives.md +129 -0
  9. data/docs/directives.md +298 -0
  10. data/docs/federation.md +108 -0
  11. data/docs/filtering-and-ordering.md +128 -0
  12. data/docs/getting-started.md +152 -0
  13. data/docs/relationships.md +95 -0
  14. data/lib/lighthouse/config.rb +112 -0
  15. data/lib/lighthouse/contracts/arg_builder.rb +17 -0
  16. data/lib/lighthouse/contracts/arg_manipulator.rb +17 -0
  17. data/lib/lighthouse/contracts/directive.rb +17 -0
  18. data/lib/lighthouse/contracts/field_manipulator.rb +17 -0
  19. data/lib/lighthouse/contracts/field_middleware.rb +16 -0
  20. data/lib/lighthouse/contracts/field_resolver.rb +18 -0
  21. data/lib/lighthouse/dataloader_sources/association_loader.rb +25 -0
  22. data/lib/lighthouse/dataloader_sources/model_loader.rb +19 -0
  23. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +86 -0
  24. data/lib/lighthouse/directive_resolvers/all_resolver.rb +73 -0
  25. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +44 -0
  26. data/lib/lighthouse/directive_resolvers/base.rb +92 -0
  27. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +56 -0
  28. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +47 -0
  29. data/lib/lighthouse/directive_resolvers/builder_applier.rb +63 -0
  30. data/lib/lighthouse/directive_resolvers/can_resolver.rb +86 -0
  31. data/lib/lighthouse/directive_resolvers/count_resolver.rb +56 -0
  32. data/lib/lighthouse/directive_resolvers/field_resolver.rb +78 -0
  33. data/lib/lighthouse/directive_resolvers/find_resolver.rb +65 -0
  34. data/lib/lighthouse/directive_resolvers/first_resolver.rb +53 -0
  35. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +32 -0
  36. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +109 -0
  37. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +43 -0
  38. data/lib/lighthouse/directive_resolvers/method_resolver.rb +35 -0
  39. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +147 -0
  40. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +35 -0
  41. data/lib/lighthouse/directive_resolvers/resolution.rb +35 -0
  42. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +169 -0
  43. data/lib/lighthouse/directives/arguments/base.rb +40 -0
  44. data/lib/lighthouse/directives/arguments/filters.rb +101 -0
  45. data/lib/lighthouse/directives/arguments/order_by.rb +48 -0
  46. data/lib/lighthouse/directives/registry.rb +168 -0
  47. data/lib/lighthouse/directives/relation_directive.rb +332 -0
  48. data/lib/lighthouse/directives/where_conditions_directive.rb +208 -0
  49. data/lib/lighthouse/graphql/version.rb +7 -0
  50. data/lib/lighthouse/graphql.rb +8 -0
  51. data/lib/lighthouse/rb_lighthouse.rb +88 -0
  52. data/lib/lighthouse/reference_resolver.rb +24 -0
  53. data/lib/lighthouse/schema_factory.rb +46 -0
  54. data/lib/lighthouse/schema_generator.rb +244 -0
  55. data/lib/lighthouse/schema_implementation.rb +119 -0
  56. data/lib/lighthouse/sdl_loader.rb +27 -0
  57. data/lib/lighthouse/support/authorization.rb +26 -0
  58. data/lib/lighthouse/support/directive_args.rb +33 -0
  59. data/lib/lighthouse/support/model_resolver.rb +61 -0
  60. data/lib/lighthouse/support/naming.rb +71 -0
  61. data/lib/lighthouse/where_conditions/operator_map.rb +35 -0
  62. data/lib/lighthouse-graphql.rb +10 -0
  63. metadata +195 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db5eca820107c7975f19338d455cae4a2bfff8b76213035785ed66788ae93978
4
+ data.tar.gz: 3381c5c9242afae11d45e843d14dcdd7cd90d5b6b593fd69d33e41249745a5a8
5
+ SHA512:
6
+ metadata.gz: b65cf99a77c8ceeb78656618deb4619d70205bb6bb1dc8b45f16939c34429b4dce17f16fbe554de8978fee381a2aa39d5649a75326fcd6418e10663ef4f5222d
7
+ data.tar.gz: 47af9818c91027cde136923d4253a1e64cc969e8f8a0aaf7f1fd61332cbd32c982354a528c15cb5467767440439b7df4d8ede19e9d6b9a607531afab743fa7f4
data/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to `lighthouse-graphql` are documented here. The format is
4
+ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
5
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-09-23
10
+
11
+ First release on RubyGems. Everything below is what the gem ships with; the library
12
+ had been running inside a private monorepo as a path gem before being extracted.
13
+
14
+ ### Added
15
+
16
+ - Registry-driven directive dispatch (`Lighthouse::DirectiveRegistry`): adding a
17
+ directive is "write one class and register it" — no edits to the generator.
18
+ - Directive contracts (`Lighthouse::Contracts::*`) mirroring PHP Lighthouse's
19
+ interfaces: `FieldResolver`, `FieldMiddleware`, `FieldManipulator`,
20
+ `ArgManipulator`, `ArgBuilder`.
21
+ - Read directives: `@all`, `@find`, `@first`, `@paginate`, `@count`,
22
+ `@aggregate`, `@hasMany`, `@hasOne`, `@belongsTo`, `@belongsToMany`,
23
+ `@field`, `@rename`, `@auth`, `@builder`, `@orderBy`, and the argument
24
+ filters `@eq`, `@neq`, `@in`, `@notIn`, `@like`, `@where`, plus
25
+ `@whereConditions` / `@whereHasConditions`.
26
+ - `@method(name:)` — resolve a field by calling a method on the parent object
27
+ (PHP-parity); removes one-line "call a method" resolvers.
28
+ - Authorization directives `@guard` (require authentication) and `@can`
29
+ (policy-based authorization, Pundit by default) with configurable
30
+ `authenticated_user` / `policy_user` / `authorizer` seams — declare auth in
31
+ the schema instead of in resolvers.
32
+ - `BuilderApplier` / argument builders now re-raise `GraphQL::ExecutionError`
33
+ instead of swallowing it, so authorization/scoping guards in a builder cannot
34
+ be silently bypassed.
35
+ - Central naming module (`Lighthouse::Support::Naming`): one rule mapping GraphQL
36
+ camelCase to Rails snake_case attributes/columns, with `@rename` as the
37
+ explicit override.
38
+ - Programmatic directive SDL: registered directives contribute their own
39
+ `directive @name(...) on ...` definitions, so apps need not hand-declare them.
40
+ - Apollo Federation entity binding: `@key`/`@extends`/`@shareable`/
41
+ `@inaccessible` and `resolve_reference`, with a default model-lookup fallback
42
+ for direct-model entities and a `ReferenceResolver` registry for custom ones.
43
+ - Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
44
+ `on_error`, and `DirectiveRegistry.register` for app-defined directives.
45
+
46
+ [Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.0...HEAD
47
+ [0.1.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vaz Innovation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # Lighthouse for Ruby (`lighthouse-graphql`)
2
+
3
+ **SDL-first, directive-driven GraphQL for Ruby on Rails.**
4
+
5
+ Lighthouse lets you build a GraphQL API for your Rails app primarily through your
6
+ **schema** (SDL) and a set of server-side **directives** — `@all`, `@find`,
7
+ `@paginate`, `@hasMany`, `@belongsTo`, `@whereConditions`, `@orderBy`, `@field`,
8
+ `@auth` and more — instead of hand-writing a resolver for every field. It is a
9
+ Ruby adaptation of the PHP [Lighthouse](https://lighthouse-php.com) library,
10
+ built on top of [graphql-ruby](https://graphql-ruby.org), with first-class
11
+ **Apollo Federation** support so a Rails service can act as a subgraph.
12
+
13
+ ```graphql
14
+ type Query {
15
+ users: [User!]! @all
16
+ user(id: ID! @eq): User @find
17
+ }
18
+
19
+ type User {
20
+ id: ID!
21
+ customAttributes: String # resolves to user.custom_attributes automatically
22
+ posts: [Post!]! @hasMany # batched via a Dataloader
23
+ }
24
+ ```
25
+
26
+ That's the whole resolver layer for those fields. No Ruby classes required.
27
+
28
+ ---
29
+
30
+ ## Why Lighthouse?
31
+
32
+ Writing a resolver class for every field is repetitive. The vast majority of
33
+ GraphQL fields do one of a handful of things: list a model, find one by id,
34
+ paginate, walk a relationship, or read an attribute. Lighthouse — like its PHP
35
+ inspiration — captures those patterns as **directives** you attach in the schema,
36
+ and falls back to a sensible default reader for plain fields. You write Ruby only
37
+ for the genuinely custom parts (`@field(resolver: "...")`), and your schema stays
38
+ the single source of truth.
39
+
40
+ ### Inspirations
41
+
42
+ - **[Lighthouse (PHP)](https://lighthouse-php.com)** — the directive vocabulary,
43
+ the SDL-first philosophy, and the arg-resolver / nested-mutation ideas.
44
+ - **[graphql-ruby](https://graphql-ruby.org)** — the execution engine,
45
+ `Dataloader`, and the connection/pagination types Lighthouse builds on.
46
+ - **[Apollo Federation](https://www.apollographql.com/docs/federation/)** — so a
47
+ Rails app can be one subgraph in a larger supergraph.
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ Add the gem to your `Gemfile`:
54
+
55
+ ```ruby
56
+ gem 'lighthouse-graphql'
57
+ ```
58
+
59
+ It depends on `graphql ~> 2.5` and `activesupport`. For federation you also need
60
+ `apollo-federation` in your app (it is an optional, app-provided dependency).
61
+
62
+ ## Quick start
63
+
64
+ 1. **Put your SDL** in `app/graphql` or any folder (default: `RAILS_ROOT/graphql`),
65
+ split across as many `.graphql` files as you like:
66
+
67
+ ```graphql
68
+ # graphql/users.graphql
69
+ type Query {
70
+ users: [User!]! @paginate
71
+ user(id: ID! @eq): User @find
72
+ }
73
+
74
+ type User {
75
+ id: ID!
76
+ name: String
77
+ email: String
78
+ posts: [Post!]! @hasMany
79
+ }
80
+ ```
81
+
82
+ 2. **Build the schema** (typically in `app/graphql/your_schema.rb`):
83
+
84
+ ```ruby
85
+ require 'lighthouse-graphql'
86
+
87
+ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
88
+ sdl_folder: Rails.root.join('graphql').to_s,
89
+ options: { base_types: { object: BaseObject } } # your graphql-ruby base classes
90
+ )
91
+ ```
92
+
93
+ 3. **Execute** it from your controller exactly like any graphql-ruby schema:
94
+
95
+ ```ruby
96
+ AppSchema.execute(params[:query], variables: params[:variables], context: { current_user: current_user })
97
+ ```
98
+
99
+ See **[docs/getting-started.md](docs/getting-started.md)** for the full setup,
100
+ including base types and configuration.
101
+
102
+ ---
103
+
104
+ ## Documentation
105
+
106
+ | Guide | What it covers |
107
+ | --- | --- |
108
+ | [Getting started](docs/getting-started.md) | Install, base types, building & serving the schema |
109
+ | [Directives reference](docs/directives.md) | Every built-in directive, grouped by purpose |
110
+ | [Relationships](docs/relationships.md) | `@hasMany`, `@belongsTo`, `@belongsToMany`, `@hasOne` and batching |
111
+ | [Filtering & ordering](docs/filtering-and-ordering.md) | `@eq`/`@where`/`@in`/`@like`, `@whereConditions`, `@orderBy` |
112
+ | [Authentication & authorization](docs/authorization.md) | `@guard`, `@can` (policy-based, Pundit by default) |
113
+ | [Apollo Federation](docs/federation.md) | `@key`, `resolve_reference`, the `ReferenceResolver` registry |
114
+ | [Custom directives](docs/custom-directives.md) | The contracts, the registry, writing your own directive |
115
+ | [Configuration](docs/configuration.md) | `Lighthouse.configure`, namespaces, error handling |
116
+ | [Best practices](docs/best-practices.md) | Conventions worth adopting |
117
+
118
+ ---
119
+
120
+ ## How it works (one paragraph)
121
+
122
+ Lighthouse parses your SDL, runs a **manipulation phase** (directives that need to
123
+ reshape the schema — e.g. `@paginate` injecting a Paginator type, `@whereConditions`
124
+ generating input types), builds an executable schema with
125
+ `GraphQL::Schema.from_definition`, then runs a **resolution phase** that attaches
126
+ behavior to fields (directives that resolve data, wrap resolvers, or compose query
127
+ constraints). Every directive is a small class registered with a
128
+ `Lighthouse::DirectiveRegistry`; adding one is "write a class and register it." A
129
+ single `Support::Naming` rule maps GraphQL `camelCase` to Rails `snake_case`, so
130
+ attribute reads "just work" with `@rename` as the explicit override.
131
+
132
+ ## License
133
+
134
+ MIT. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,109 @@
1
+ # Authentication & Authorization
2
+
3
+ Lighthouse lets you enforce **authentication** and **authorization** declaratively
4
+ in your schema with the `@guard` and `@can` directives — instead of hand-writing
5
+ a resolver whose only job is a policy check. This mirrors PHP Lighthouse's
6
+ `@guard` / `@can*` family, adapted to Ruby and your authorization library (Pundit
7
+ by default).
8
+
9
+ ## Configuration
10
+
11
+ The library is framework-agnostic: how it reads the current user and how it
12
+ checks an ability are both configurable. The defaults work with a Rails app that
13
+ puts `current_user` / `pundit_user` in the GraphQL context and uses Pundit, so
14
+ **typically no configuration is needed**:
15
+
16
+ ```ruby
17
+ Lighthouse.configure do |config|
18
+ # The authenticated user (nil if none). Default:
19
+ config.authenticated_user = ->(ctx) { ctx[:current_user] }
20
+
21
+ # The "user" passed to the authorizer. Default: ctx[:pundit_user], falling back
22
+ # to the authenticated user.
23
+ config.policy_user = ->(ctx) { ctx[:pundit_user] }
24
+
25
+ # How an ability is checked. Default uses Pundit:
26
+ config.authorizer = ->(user, record, ability) { Pundit.policy!(user, record).public_send("#{ability}?") }
27
+ end
28
+ ```
29
+
30
+ Point `authorizer` at any system (CanCanCan, ActionPolicy, a plain lambda) to use
31
+ something other than Pundit.
32
+
33
+ ## `@guard` — require authentication
34
+
35
+ Require an authenticated user before a field resolves. Raises `Unauthenticated.`
36
+ otherwise.
37
+
38
+ ```graphql
39
+ type Query {
40
+ me: User @field(resolver: "Resolvers::CurrentUser") @guard
41
+ }
42
+ ```
43
+
44
+ `@guard` runs **outermost** — before any other directive on the field.
45
+
46
+ ## `@can` — authorize with a policy
47
+
48
+ Check a policy ability. By default it authorizes the **resolved record**: the
49
+ field resolves first, then `@can` checks the ability against the result (and
50
+ raises if denied). `nil` results pass through; collections are left to scoping.
51
+
52
+ ```graphql
53
+ type Query {
54
+ # Resolve the (account-scoped) contact, then authorize ContactPolicy#show?
55
+ contact(id: ID!): Contact
56
+ @field(resolver: "Resolvers::ContactResolver")
57
+ @can(ability: "show")
58
+ }
59
+ ```
60
+
61
+ ```ruby
62
+ # The resolver is now just a scoped finder — no authorization boilerplate.
63
+ module Resolvers
64
+ class ContactResolver
65
+ def self.call(_obj, args, ctx)
66
+ account = ctx[:current_account]
67
+ account.contacts.find_by(id: args[:id]) || raise(GraphQL::ExecutionError, 'Contact not found.')
68
+ end
69
+ end
70
+ end
71
+ ```
72
+
73
+ With Pundit, `@can(ability: "show")` calls `ContactPolicy.new(pundit_user, contact).show?`.
74
+
75
+ ### Other modes
76
+
77
+ ```graphql
78
+ # Authorize against the parent object (e.g. restrict a field on a type):
79
+ type User {
80
+ email: String! @can(ability: "view_email", root: true)
81
+ }
82
+
83
+ # Find a model by an argument and authorize it before resolving:
84
+ type Mutation {
85
+ archivePost(id: ID!): Post @can(ability: "update", find: "id", model: "Post") @field(resolver: "...")
86
+ }
87
+ ```
88
+
89
+ `@can` is `repeatable`, so you can require multiple abilities.
90
+
91
+ ## Multi-tenancy vs. authorization
92
+
93
+ Keep the two layers distinct:
94
+
95
+ - **Tenancy / scoping** (which account's records are visible) belongs in your
96
+ resolver or an `@builder` — e.g. `account.contacts`. This is application-specific.
97
+ - **Authorization** (may this user perform this action on this record) belongs in
98
+ `@can` + your policies.
99
+
100
+ For list fields, filter with a policy **scope** in a builder (e.g. Pundit's
101
+ `policy_scope`) rather than authorizing item-by-item.
102
+
103
+ ## Ordering
104
+
105
+ When several wrapping directives are on one field, they run in this order:
106
+
107
+ 1. `@guard` / `@auth` (authentication) — outermost
108
+ 2. `@can` (authorization of the resolved value)
109
+ 3. the field's data resolver (`@field`, `@all`, `@hasMany`, …) — innermost
@@ -0,0 +1,75 @@
1
+ # Best Practices
2
+
3
+ A few conventions that keep a Lighthouse schema clean and predictable. They build
4
+ on the [GraphQL best practices](https://graphql-rules.com/) and PHP Lighthouse's
5
+ guidance, adapted to Rails.
6
+
7
+ ## Name your primary key `id`
8
+
9
+ Clients and tooling (and Apollo Federation) assume an `id` field. Keep it `ID!`.
10
+
11
+ ## Let the schema be the source of truth
12
+
13
+ Reach for a directive before writing a resolver class. Most fields are an `@all`,
14
+ `@find`, `@paginate`, a relationship, or a plain attribute read. Hand-write Ruby
15
+ (`@field`) only for genuinely custom logic. Your `.graphql` files should read like
16
+ documentation of your API.
17
+
18
+ ## Keep the camelCase ↔ snake_case mapping implicit
19
+
20
+ Expose `camelCase` fields and let the default reader map them to `snake_case`
21
+ attributes. Only use `@rename(attribute:)` when the mapping isn't a plain
22
+ `underscore` — don't sprinkle it everywhere.
23
+
24
+ ## Restrict filterable columns
25
+
26
+ When using `@whereConditions`, always pass `columns:` (or `columnsEnum:`) to
27
+ constrain what clients can filter on. Open-ended column filtering is a performance
28
+ and security risk:
29
+
30
+ ```graphql
31
+ people(where: _ @whereConditions(columns: ["age", "type"])): [Person!]! @all
32
+ ```
33
+
34
+ The same applies to `@orderBy` — expose a bounded set of sortable columns.
35
+
36
+ ## Enable the Dataloader
37
+
38
+ Add `use GraphQL::Dataloader` to your schema so relationship directives batch.
39
+ Without it, nested relationships re-introduce N+1 queries.
40
+
41
+ ## Authorize at the field, early
42
+
43
+ Use `@auth` (and your own authorization directives) on fields that require it.
44
+ Because `@auth` wraps the resolver, the check runs before any data is loaded.
45
+
46
+ ## Mutations: prefer throwable errors
47
+
48
+ For mutations, raise `GraphQL::ExecutionError` for business failures ("Not
49
+ found", validation messages) rather than returning an `errors` field in the
50
+ payload. Thrown errors surface in the response's top-level `errors` array, which
51
+ is where GraphQL clients' error handling already looks — `onError` callbacks fire,
52
+ and you don't have to thread an `errors` field through every payload and caller.
53
+
54
+ ```ruby
55
+ def resolve(id:)
56
+ record = Model.find_by(id: id)
57
+ raise GraphQL::ExecutionError, 'Not found.' unless record
58
+ # ...
59
+ { record: record }
60
+ end
61
+ ```
62
+
63
+ ## One configuration seam
64
+
65
+ Keep all Lighthouse wiring — `resolver_namespaces`, custom directive
66
+ registration, federation reference resolvers, `on_error` — in initializers, not
67
+ scattered through the app. This keeps the library a clean dependency and makes the
68
+ app's GraphQL setup easy to find.
69
+
70
+ ## Split your SDL
71
+
72
+ Lighthouse concatenates every `*.graphql` file under your `sdl_folder`. Organize
73
+ by domain (`users.graphql`, `orders.graphql`, …) and keep a small
74
+ `_directives.graphql` only for directives you declare yourself (built-ins are
75
+ injected automatically).
@@ -0,0 +1,70 @@
1
+ # Configuration
2
+
3
+ Configure Lighthouse from a single initializer. Keeping app-specific wiring here
4
+ means the library holds no app constants — which is what makes it cleanly
5
+ reusable and publishable.
6
+
7
+ ```ruby
8
+ # config/initializers/lighthouse.rb
9
+ require 'lighthouse-graphql'
10
+
11
+ Lighthouse.configure do |config|
12
+ config.sdl_folder = Rails.root.join('graphql').to_s
13
+ config.resolver_namespaces = ['App::Graphql', 'Resolvers']
14
+ config.on_error = ->(error, _context) { Rails.error.report(error) }
15
+ end
16
+ ```
17
+
18
+ ## Options
19
+
20
+ ### `sdl_folder`
21
+
22
+ Where Lighthouse loads `*.graphql` files from. Defaults to `RAILS_ROOT/graphql`
23
+ in a Rails app, `nil` otherwise (pass SDL explicitly via `SchemaFactory` when not
24
+ on Rails).
25
+
26
+ ### `resolver_namespaces`
27
+
28
+ An ordered list of namespaces tried when resolving a **class name** referenced by
29
+ a directive argument — `@field(resolver: "ContactPerson")`, `@all(model: "User")`,
30
+ `@builder(class: "ContactsBuilder")`. Fully-qualified names (containing `::`) are
31
+ used as-is. Defaults to `['App::Graphql']`.
32
+
33
+ ```ruby
34
+ config.resolver_namespaces = ['App::Graphql', 'Resolvers']
35
+ # "ContactPerson" tries App::Graphql::ContactPerson, then Resolvers::ContactPerson, then ::ContactPerson
36
+ ```
37
+
38
+ This is the seam that keeps the gem free of your app's namespace.
39
+
40
+ ### `on_error`
41
+
42
+ A callable `->(error, context) { ... }` invoked when a directive or resolver
43
+ swallows an error (e.g. a malformed filter). The default **raises in test** (so
44
+ failures surface in your suite) and logs otherwise. Point it at your error
45
+ reporter in production:
46
+
47
+ ```ruby
48
+ config.on_error = ->(error, _ctx) { Sentry.capture_exception(error) }
49
+ ```
50
+
51
+ ## Extending the registry
52
+
53
+ Register app-defined directives (see [custom-directives.md](custom-directives.md))
54
+ from the same initializer:
55
+
56
+ ```ruby
57
+ Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::StartsWith)
58
+ ```
59
+
60
+ ## Federation reference resolvers
61
+
62
+ Custom federation entity resolvers are registered with
63
+ `Lighthouse::ReferenceResolver` (see [federation.md](federation.md)), typically in
64
+ `config/initializers/federation.rb` so they re-register on code reload:
65
+
66
+ ```ruby
67
+ Rails.application.config.to_prepare do
68
+ Lighthouse::ReferenceResolver.register('User') { |ref, _ctx| User.find_by(id: ref['id']) }
69
+ end
70
+ ```
@@ -0,0 +1,129 @@
1
+ # Custom Directives
2
+
3
+ Adding a directive to Lighthouse is "write one class and register it." There are
4
+ no edits to Lighthouse internals — the generator discovers behavior through a set
5
+ of **contracts** (marker modules) and a **registry**.
6
+
7
+ ## The architecture
8
+
9
+ Lighthouse builds a schema in two phases, mirroring PHP Lighthouse's
10
+ manipulator/resolver split:
11
+
12
+ 1. **Manipulation phase** (before the schema is built) — directives that need to
13
+ reshape the SDL/AST run here: `@paginate` injecting a Paginator type,
14
+ `@whereConditions` generating input and enum types, etc.
15
+ 2. **Resolution phase** (after `GraphQL::Schema.from_definition`) — directives
16
+ attach runtime behavior: resolving data, wrapping a resolver, or composing a
17
+ query constraint.
18
+
19
+ A `Lighthouse::DirectiveRegistry` maps directive names to the classes that
20
+ implement them. The generator iterates the registry rather than a hardcoded
21
+ dispatch table.
22
+
23
+ ## The contracts
24
+
25
+ Include the marker module(s) your directive fulfils:
26
+
27
+ | Contract | Role | Key method |
28
+ | --- | --- | --- |
29
+ | `Contracts::FieldResolver` | provide a field's resolver | `#define_resolver(type)` (or `#resolve_field`) |
30
+ | `Contracts::FieldMiddleware` | wrap an existing resolver (e.g. `@auth`) | `#wrap_resolver(next)` |
31
+ | `Contracts::FieldManipulator` | reshape the schema for a field | `manipulate_field_definition(...)` |
32
+ | `Contracts::ArgManipulator` | reshape the schema for an argument | `manipulate_argument_definition(...)` |
33
+ | `Contracts::ArgBuilder` | compose a query constraint from an argument | `#handle_builder(relation, value)` |
34
+
35
+ A directive class also answers two class-level questions: its name and its SDL
36
+ definition.
37
+
38
+ ```ruby
39
+ def self.directive_name = 'upcase'
40
+ def self.definition = 'directive @upcase on FIELD_DEFINITION'
41
+ ```
42
+
43
+ Lighthouse injects that `definition` into the schema automatically, so apps never
44
+ hand-declare your directive.
45
+
46
+ ## Example 1 — an argument filter
47
+
48
+ Argument-builder directives are the simplest. Suppose you want `@startsWith`:
49
+
50
+ ```ruby
51
+ module App
52
+ module Graphql
53
+ module Directives
54
+ class StartsWith < Lighthouse::GraphQL::Directives::Arguments::Base
55
+ def self.directive_name = 'startsWith'
56
+ def self.definition = 'directive @startsWith(key: String) on ARGUMENT_DEFINITION'
57
+
58
+ def handle_builder(relation, value)
59
+ return relation if value.nil?
60
+
61
+ relation.where(arel_column(relation).matches("#{value}%"))
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ ```
68
+
69
+ `Arguments::Base` gives you `column` (the `key:` override or snake_cased arg name)
70
+ and `arel_column(relation)`.
71
+
72
+ ## Example 2 — a field resolver
73
+
74
+ A field-resolver directive attaches a resolver to a field. The simplest path is to
75
+ subclass the base resolver and implement `define_resolver`:
76
+
77
+ ```ruby
78
+ module App
79
+ module Graphql
80
+ module Directives
81
+ class UpcaseDirective < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
82
+ def self.directive_name = 'upcase'
83
+ def self.definition = 'directive @upcase on FIELD_DEFINITION'
84
+
85
+ def define_resolver(target_type)
86
+ name = @field_def.name
87
+ target_type.define_singleton_method("resolve_field_#{name}") do |obj, _args, _ctx|
88
+ value = obj.public_send(Lighthouse::Support::Naming.attribute(name))
89
+ value&.upcase
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## Registering
99
+
100
+ Register your directive once, from your Lighthouse initializer:
101
+
102
+ ```ruby
103
+ # config/initializers/lighthouse.rb
104
+ Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::StartsWith)
105
+ Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::UpcaseDirective)
106
+ ```
107
+
108
+ It is now usable in your SDL — no other changes:
109
+
110
+ ```graphql
111
+ type Query {
112
+ users(name: String @startsWith): [User!]! @all
113
+ }
114
+
115
+ type User {
116
+ shoutName: String @upcase @rename(attribute: "name")
117
+ }
118
+ ```
119
+
120
+ ## Naming helper
121
+
122
+ Whenever you map a GraphQL name to a database column or model attribute, route
123
+ through `Lighthouse::Support::Naming` so your directive honors the same
124
+ camelCase→snake_case rule as the rest of the library:
125
+
126
+ ```ruby
127
+ Lighthouse::Support::Naming.attribute('customAttributes') # => "custom_attributes"
128
+ Lighthouse::Support::Naming.column('CREATED_AT') # => "created_at"
129
+ ```