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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/docs/authorization.md +109 -0
- data/docs/best-practices.md +75 -0
- data/docs/configuration.md +70 -0
- data/docs/custom-directives.md +129 -0
- data/docs/directives.md +298 -0
- data/docs/federation.md +108 -0
- data/docs/filtering-and-ordering.md +128 -0
- data/docs/getting-started.md +152 -0
- data/docs/relationships.md +95 -0
- data/lib/lighthouse/config.rb +112 -0
- data/lib/lighthouse/contracts/arg_builder.rb +17 -0
- data/lib/lighthouse/contracts/arg_manipulator.rb +17 -0
- data/lib/lighthouse/contracts/directive.rb +17 -0
- data/lib/lighthouse/contracts/field_manipulator.rb +17 -0
- data/lib/lighthouse/contracts/field_middleware.rb +16 -0
- data/lib/lighthouse/contracts/field_resolver.rb +18 -0
- data/lib/lighthouse/dataloader_sources/association_loader.rb +25 -0
- data/lib/lighthouse/dataloader_sources/model_loader.rb +19 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +86 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +73 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +44 -0
- data/lib/lighthouse/directive_resolvers/base.rb +92 -0
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +56 -0
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +47 -0
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +63 -0
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +86 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +56 -0
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +78 -0
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +65 -0
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +53 -0
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +32 -0
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +109 -0
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +35 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +147 -0
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +35 -0
- data/lib/lighthouse/directive_resolvers/resolution.rb +35 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +169 -0
- data/lib/lighthouse/directives/arguments/base.rb +40 -0
- data/lib/lighthouse/directives/arguments/filters.rb +101 -0
- data/lib/lighthouse/directives/arguments/order_by.rb +48 -0
- data/lib/lighthouse/directives/registry.rb +168 -0
- data/lib/lighthouse/directives/relation_directive.rb +332 -0
- data/lib/lighthouse/directives/where_conditions_directive.rb +208 -0
- data/lib/lighthouse/graphql/version.rb +7 -0
- data/lib/lighthouse/graphql.rb +8 -0
- data/lib/lighthouse/rb_lighthouse.rb +88 -0
- data/lib/lighthouse/reference_resolver.rb +24 -0
- data/lib/lighthouse/schema_factory.rb +46 -0
- data/lib/lighthouse/schema_generator.rb +244 -0
- data/lib/lighthouse/schema_implementation.rb +119 -0
- data/lib/lighthouse/sdl_loader.rb +27 -0
- data/lib/lighthouse/support/authorization.rb +26 -0
- data/lib/lighthouse/support/directive_args.rb +33 -0
- data/lib/lighthouse/support/model_resolver.rb +61 -0
- data/lib/lighthouse/support/naming.rb +71 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +35 -0
- data/lib/lighthouse-graphql.rb +10 -0
- 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
|
+
```
|