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
data/docs/directives.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Directives Reference
|
|
2
|
+
|
|
3
|
+
Directives are how you add behavior to your schema. They begin with `@` and are
|
|
4
|
+
attached to fields or arguments. Lighthouse **contributes the SDL definitions for
|
|
5
|
+
its own directives automatically**, so you don't have to declare
|
|
6
|
+
`directive @all(...) on ...` yourself — just use them.
|
|
7
|
+
|
|
8
|
+
Below is every built-in directive, grouped by purpose. Federation directives are
|
|
9
|
+
documented separately in [federation.md](federation.md).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Query / fetch directives
|
|
14
|
+
|
|
15
|
+
These resolve a root (or any) field from a model.
|
|
16
|
+
|
|
17
|
+
### `@all`
|
|
18
|
+
|
|
19
|
+
Return all records of a model.
|
|
20
|
+
|
|
21
|
+
```graphql
|
|
22
|
+
type Query {
|
|
23
|
+
users: [User!]! @all
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The model defaults to the field's return type (`User`). Override with
|
|
28
|
+
`@all(model: "Account")`. Combine with [argument filters](filtering-and-ordering.md)
|
|
29
|
+
and [`@orderBy`](#orderby) to constrain the result.
|
|
30
|
+
|
|
31
|
+
### `@find`
|
|
32
|
+
|
|
33
|
+
Return a single record, typically by id. Pair it with an argument filter such as
|
|
34
|
+
[`@eq`](#eq) so Lighthouse knows what to match on.
|
|
35
|
+
|
|
36
|
+
```graphql
|
|
37
|
+
type Query {
|
|
38
|
+
user(id: ID! @eq): User @find
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `@first`
|
|
43
|
+
|
|
44
|
+
Like `@all`, but returns the **first** matching record instead of a list. Useful
|
|
45
|
+
with filters to fetch one record by criteria.
|
|
46
|
+
|
|
47
|
+
```graphql
|
|
48
|
+
type Query {
|
|
49
|
+
latestPost(authorId: ID! @eq): Post @first
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `@paginate`
|
|
54
|
+
|
|
55
|
+
Return a paginated list. Lighthouse rewrites the field to a Paginator type and
|
|
56
|
+
adds pagination arguments.
|
|
57
|
+
|
|
58
|
+
```graphql
|
|
59
|
+
type Query {
|
|
60
|
+
posts: [Post!]! @paginate
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
becomes, in effect:
|
|
65
|
+
|
|
66
|
+
```graphql
|
|
67
|
+
type Query {
|
|
68
|
+
posts(first: Int, after: String): PostConnection
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use `@paginate(type: "CONNECTION")` for Relay-style cursor connections, or the
|
|
73
|
+
default offset-based paginator otherwise.
|
|
74
|
+
|
|
75
|
+
### `@count`
|
|
76
|
+
|
|
77
|
+
Return the number of records — of a model, or of a relation on the parent object.
|
|
78
|
+
|
|
79
|
+
```graphql
|
|
80
|
+
type Query {
|
|
81
|
+
userCount: Int! @count(model: "User")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type Account {
|
|
85
|
+
usersCount: Int! @count(relation: "users")
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `@aggregate`
|
|
90
|
+
|
|
91
|
+
Compute an aggregate (`SUM`, `AVG`, `MIN`, `MAX`, `COUNT`) over a column.
|
|
92
|
+
|
|
93
|
+
```graphql
|
|
94
|
+
type Account {
|
|
95
|
+
totalRevenue: Float @aggregate(relation: "orders", column: "amount", function: SUM)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Relationship directives
|
|
102
|
+
|
|
103
|
+
Resolve ActiveRecord associations, batched through a Dataloader to avoid N+1.
|
|
104
|
+
See [relationships.md](relationships.md) for the full guide.
|
|
105
|
+
|
|
106
|
+
| Directive | Association |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `@hasMany` | one-to-many (returns a connection) |
|
|
109
|
+
| `@hasOne` | one-to-one |
|
|
110
|
+
| `@belongsTo` | inverse of has-one/has-many |
|
|
111
|
+
| `@belongsToMany` | many-to-many |
|
|
112
|
+
|
|
113
|
+
```graphql
|
|
114
|
+
type User {
|
|
115
|
+
posts: [Post!]! @hasMany
|
|
116
|
+
profile: Profile @hasOne
|
|
117
|
+
account: Account! @belongsTo
|
|
118
|
+
roles: [Role!]! @belongsToMany
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The relation name defaults to the field name (snake_cased). Override with
|
|
123
|
+
`@hasMany(relation: "authored_posts")`.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Field directives
|
|
128
|
+
|
|
129
|
+
### `@field`
|
|
130
|
+
|
|
131
|
+
Delegate a field to a custom resolver class — your escape hatch for anything the
|
|
132
|
+
built-in directives don't cover.
|
|
133
|
+
|
|
134
|
+
```graphql
|
|
135
|
+
type Contact {
|
|
136
|
+
person: Person @field(resolver: "Resolvers::ContactPerson")
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The resolver is resolved against your configured
|
|
141
|
+
[`resolver_namespaces`](configuration.md). It may be a class with a class-level
|
|
142
|
+
`call`, an instance `#call`, or an instance `#resolve`, each receiving
|
|
143
|
+
`(object, args, context)`:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
module Resolvers
|
|
147
|
+
class ContactPerson
|
|
148
|
+
def call(contact, _args, _ctx)
|
|
149
|
+
contact.linked_person
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `@rename`
|
|
156
|
+
|
|
157
|
+
Read a differently-named attribute off the parent object. This is the explicit
|
|
158
|
+
override of the default camelCase→snake_case reader.
|
|
159
|
+
|
|
160
|
+
```graphql
|
|
161
|
+
type User {
|
|
162
|
+
displayName: String @rename(attribute: "full_name")
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `@method`
|
|
167
|
+
|
|
168
|
+
Resolve a field by calling a method on the parent object (PHP-parity). Great for
|
|
169
|
+
exposing a computed method or predicate without a custom resolver.
|
|
170
|
+
|
|
171
|
+
```graphql
|
|
172
|
+
type Conversation {
|
|
173
|
+
labels: [String!]! @method(name: "label_list")
|
|
174
|
+
canReply: Boolean @method(name: "can_reply?")
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Authentication & Authorization
|
|
181
|
+
|
|
182
|
+
Enforce auth declaratively instead of in resolvers. Full guide:
|
|
183
|
+
[authorization.md](authorization.md).
|
|
184
|
+
|
|
185
|
+
### `@guard`
|
|
186
|
+
|
|
187
|
+
Require an authenticated user before the field resolves; otherwise raise
|
|
188
|
+
`Unauthenticated.`. Runs outermost.
|
|
189
|
+
|
|
190
|
+
```graphql
|
|
191
|
+
type Query {
|
|
192
|
+
me: User @field(resolver: "Resolvers::CurrentUser") @guard
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `@can`
|
|
197
|
+
|
|
198
|
+
Authorize a field with a policy ability (Pundit by default; configurable). By
|
|
199
|
+
default it authorizes the resolved record. `repeatable`.
|
|
200
|
+
|
|
201
|
+
```graphql
|
|
202
|
+
type Query {
|
|
203
|
+
contact(id: ID!): Contact
|
|
204
|
+
@field(resolver: "Resolvers::ContactResolver")
|
|
205
|
+
@can(ability: "show")
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`@can(ability:, find:, model:, root:)` — see [authorization.md](authorization.md)
|
|
210
|
+
for the `find`/`model`/`root` modes.
|
|
211
|
+
|
|
212
|
+
### `@auth`
|
|
213
|
+
|
|
214
|
+
The original guard directive — require an authenticated user
|
|
215
|
+
(`context[:current_user]`) before the field resolves; otherwise raise
|
|
216
|
+
`Unauthenticated.`. `@guard` is the preferred, configurable equivalent.
|
|
217
|
+
|
|
218
|
+
```graphql
|
|
219
|
+
type Query {
|
|
220
|
+
me: User @auth
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Query builder customization
|
|
227
|
+
|
|
228
|
+
### `@builder`
|
|
229
|
+
|
|
230
|
+
Apply a custom method to the query builder before the field resolves. Repeatable.
|
|
231
|
+
|
|
232
|
+
```graphql
|
|
233
|
+
type Query {
|
|
234
|
+
contacts: [Contact!]! @paginate @builder(class: "Resolvers::ContactsBuilder")
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
module Resolvers
|
|
240
|
+
class ContactsBuilder
|
|
241
|
+
def self.call(relation, args, _ctx)
|
|
242
|
+
args[:unassigned] ? relation.left_joins(:assignments).where(assignments: { id: nil }) : relation
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Argument filters
|
|
251
|
+
|
|
252
|
+
Attach these to a **field argument** to compose a `WHERE`/`ORDER BY` clause onto
|
|
253
|
+
the query when the client supplies that argument. They compose in argument order.
|
|
254
|
+
Full guide: [filtering-and-ordering.md](filtering-and-ordering.md).
|
|
255
|
+
|
|
256
|
+
| Directive | Effect |
|
|
257
|
+
| --- | --- |
|
|
258
|
+
| `@eq` | `column = value` |
|
|
259
|
+
| `@neq` | `column != value` |
|
|
260
|
+
| `@in` | `column IN (values)` |
|
|
261
|
+
| `@notIn` | `column NOT IN (values)` |
|
|
262
|
+
| `@like` | `column LIKE %value%` |
|
|
263
|
+
| `@where(operator:)` | generic operator (`GT`, `GTE`, `IN`, `IS_NULL`, `CONTAINS`, …) |
|
|
264
|
+
| `@orderBy` | sort by one or more columns |
|
|
265
|
+
|
|
266
|
+
```graphql
|
|
267
|
+
type Query {
|
|
268
|
+
users(
|
|
269
|
+
status: String @eq
|
|
270
|
+
nameSearch: String @like
|
|
271
|
+
minAge: Int @where(operator: "GTE", key: "age")
|
|
272
|
+
orderBy: [OrderByClause!] @orderBy
|
|
273
|
+
): [User!]! @all
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The column defaults to the snake_cased argument name; override with `key:`.
|
|
278
|
+
|
|
279
|
+
### `@whereConditions` / `@whereHasConditions`
|
|
280
|
+
|
|
281
|
+
Give clients a structured, dynamic `WHERE` builder (AND/OR/operators, and HAS for
|
|
282
|
+
related records). Lighthouse generates the input and enum types for you. See
|
|
283
|
+
[filtering-and-ordering.md](filtering-and-ordering.md#complex-where-conditions).
|
|
284
|
+
|
|
285
|
+
```graphql
|
|
286
|
+
type Query {
|
|
287
|
+
people(where: _ @whereConditions(columns: ["age", "type"])): [Person!]! @all
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Adding your own
|
|
294
|
+
|
|
295
|
+
Every directive above is a small class registered with
|
|
296
|
+
`Lighthouse::DirectiveRegistry`. To add one, implement the relevant
|
|
297
|
+
[contract](custom-directives.md) and register it — no edits to Lighthouse
|
|
298
|
+
internals. See [custom-directives.md](custom-directives.md).
|
data/docs/federation.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Apollo Federation
|
|
2
|
+
|
|
3
|
+
Lighthouse lets a Rails service act as an [Apollo Federation](https://www.apollographql.com/docs/federation/)
|
|
4
|
+
**subgraph**: it understands the federation directives in your SDL and wires up
|
|
5
|
+
entity resolution so a gateway can compose your types with other subgraphs.
|
|
6
|
+
|
|
7
|
+
This relies on the [`apollo-federation`](https://github.com/Gusto/apollo-federation-ruby)
|
|
8
|
+
gem, which you include in your app's base types and schema.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
Include the federation mixins in the base types you pass to Lighthouse, and make
|
|
13
|
+
your schema a federation schema:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
class BaseObject < GraphQL::Schema::Object
|
|
17
|
+
include ApolloFederation::Object
|
|
18
|
+
|
|
19
|
+
# Default entity resolution: look the record up by id on the model that shares
|
|
20
|
+
# the type's GraphQL name. Lighthouse falls back to this when no custom
|
|
21
|
+
# resolver is registered (see below).
|
|
22
|
+
def self.resolve_reference(reference, _context)
|
|
23
|
+
model = graphql_name.safe_constantize
|
|
24
|
+
return nil unless model&.respond_to?(:find_by)
|
|
25
|
+
|
|
26
|
+
model.find_by(id: reference[:id] || reference['id'])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
BaseAppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
31
|
+
sdl_folder: Rails.root.join('graphql').to_s,
|
|
32
|
+
options: { federation: true, base_types: { object: BaseObject } }
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
class AppSchema < BaseAppSchema
|
|
36
|
+
include ApolloFederation::Schema
|
|
37
|
+
federation(version: '2.3')
|
|
38
|
+
query(superclass.query)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Declaring entities
|
|
43
|
+
|
|
44
|
+
Mark a type as a federation entity with `@key`. Lighthouse recognizes the
|
|
45
|
+
federation directives during the resolution phase and applies them:
|
|
46
|
+
|
|
47
|
+
```graphql
|
|
48
|
+
type User @key(fields: "id") {
|
|
49
|
+
id: ID!
|
|
50
|
+
name: String
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Order @key(fields: "id") @shareable {
|
|
54
|
+
id: ID!
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Supported object-level directives: `@key`, `@extends`, `@shareable`,
|
|
59
|
+
`@inaccessible`. Field-level `@external` is recognized for federation composition.
|
|
60
|
+
|
|
61
|
+
## Entity resolution (`resolve_reference`)
|
|
62
|
+
|
|
63
|
+
When the gateway asks your subgraph to resolve an entity (via `_entities`),
|
|
64
|
+
Lighthouse routes to a `resolve_reference` it installs on each `@key` type. The
|
|
65
|
+
order of precedence is:
|
|
66
|
+
|
|
67
|
+
1. **A custom resolver** registered with `Lighthouse::ReferenceResolver` for that
|
|
68
|
+
type, if any.
|
|
69
|
+
2. **The base type's `resolve_reference`** otherwise (e.g. the default
|
|
70
|
+
model-lookup-by-id shown above).
|
|
71
|
+
|
|
72
|
+
This means direct-model entities resolve with **zero extra code** — the default
|
|
73
|
+
lookup handles them — while you register custom resolvers only for the entities
|
|
74
|
+
that need bespoke loading.
|
|
75
|
+
|
|
76
|
+
### Custom reference resolvers
|
|
77
|
+
|
|
78
|
+
For entities that aren't a straightforward `find_by(id:)` — for example, ones
|
|
79
|
+
backed by a join or an external identifier — register a resolver. The single best
|
|
80
|
+
place is an initializer:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# config/initializers/federation.rb
|
|
84
|
+
Rails.application.config.to_prepare do
|
|
85
|
+
Lighthouse::ReferenceResolver.register('JudgmentCreditor') do |reference, _context|
|
|
86
|
+
id = reference['id'] || reference[:id]
|
|
87
|
+
OpenStruct.new(id: id, contacts: Contact.joins(:links).where(links: { external_id: id }))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`register(typename) { |reference, context| ... }` returns the object that
|
|
93
|
+
represents the entity; its fields then resolve normally.
|
|
94
|
+
|
|
95
|
+
## The federation subgraph SDL
|
|
96
|
+
|
|
97
|
+
A federation subgraph publishes its capabilities via the `_service { sdl }` field
|
|
98
|
+
(and `federation_sdl`), which a gateway uses to compose the supergraph. With the
|
|
99
|
+
setup above this is produced for you, including the `@link` to the federation spec
|
|
100
|
+
and the `@key`/`@shareable`/… directives on your types.
|
|
101
|
+
|
|
102
|
+
## Gateways
|
|
103
|
+
|
|
104
|
+
Apollo Gateways that use `IntrospectAndCompose` poll subgraphs and recompose
|
|
105
|
+
automatically when your subgraph SDL changes — no manual step needed when you
|
|
106
|
+
deploy schema changes. Gateways that consume a static, pre-composed supergraph
|
|
107
|
+
file need that file recomposed (e.g. via `rover supergraph compose`) after you
|
|
108
|
+
change the subgraph.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Filtering & Ordering
|
|
2
|
+
|
|
3
|
+
Lighthouse offers two complementary ways to constrain a query: **simple argument
|
|
4
|
+
filters** (one directive per argument) and **complex where conditions** (a single
|
|
5
|
+
structured argument the client drives dynamically). Both work on any field backed
|
|
6
|
+
by a query — `@all`, `@paginate`, `@first`, `@count`, and the relationship
|
|
7
|
+
directives.
|
|
8
|
+
|
|
9
|
+
## Simple argument filters
|
|
10
|
+
|
|
11
|
+
Attach a filter directive to a field argument. When the client passes that
|
|
12
|
+
argument, Lighthouse folds the corresponding clause onto the query builder.
|
|
13
|
+
Filters compose in the order the arguments appear in the SDL.
|
|
14
|
+
|
|
15
|
+
```graphql
|
|
16
|
+
type Query {
|
|
17
|
+
users(
|
|
18
|
+
status: String @eq
|
|
19
|
+
role: [String!] @in
|
|
20
|
+
nameSearch: String @like
|
|
21
|
+
minAge: Int @where(operator: "GTE", key: "age")
|
|
22
|
+
): [User!]! @all
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Directive | SQL |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `@eq` | `column = value` |
|
|
29
|
+
| `@neq` | `column != value` |
|
|
30
|
+
| `@in` | `column IN (...)` |
|
|
31
|
+
| `@notIn` | `column NOT IN (...)` |
|
|
32
|
+
| `@like` | `column LIKE '%value%'` |
|
|
33
|
+
| `@where(operator:)` | operator-driven (see below) |
|
|
34
|
+
|
|
35
|
+
By default the column is the **snake_cased argument name** (`minAge` → `min_age`).
|
|
36
|
+
Override it with `key:` — above, `minAge` filters the `age` column.
|
|
37
|
+
|
|
38
|
+
### `@where` operators
|
|
39
|
+
|
|
40
|
+
`@where(operator: "...")` supports the same operator vocabulary as
|
|
41
|
+
`@whereConditions`:
|
|
42
|
+
|
|
43
|
+
`EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`, `IN`, `NIN`, `CONTAINS`, `STARTS_WITH`,
|
|
44
|
+
`ENDS_WITH`, `IS_NULL`, `IS_NOT_NULL`.
|
|
45
|
+
|
|
46
|
+
```graphql
|
|
47
|
+
type Query {
|
|
48
|
+
events(
|
|
49
|
+
after: String @where(operator: "GTE", key: "starts_at")
|
|
50
|
+
titleContains: String @where(operator: "CONTAINS", key: "title")
|
|
51
|
+
): [Event!]! @all
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Ordering — `@orderBy`
|
|
56
|
+
|
|
57
|
+
Sort the result by one or more columns. The client passes a list of clauses, each
|
|
58
|
+
with a `column` and an `order` (`ASC`/`DESC`); clauses apply in order.
|
|
59
|
+
|
|
60
|
+
```graphql
|
|
61
|
+
type Query {
|
|
62
|
+
users(orderBy: [OrderByClause!] @orderBy): [User!]! @all
|
|
63
|
+
|
|
64
|
+
# query:
|
|
65
|
+
# users(orderBy: [{ column: NAME, order: ASC }, { column: CREATED_AT, order: DESC }])
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`@orderBy` also accepts `{ field:, direction: }` clause shapes, so it slots into
|
|
70
|
+
existing client conventions.
|
|
71
|
+
|
|
72
|
+
## Complex where conditions
|
|
73
|
+
|
|
74
|
+
For a flexible, client-controlled `WHERE` builder, use `@whereConditions`.
|
|
75
|
+
Lighthouse generates the input and column-enum types automatically — you only
|
|
76
|
+
declare the allowed columns.
|
|
77
|
+
|
|
78
|
+
```graphql
|
|
79
|
+
type Query {
|
|
80
|
+
people(
|
|
81
|
+
where: _ @whereConditions(columns: ["age", "type", "height"])
|
|
82
|
+
): [Person!]! @all
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Clients can then build dynamic conditions with `AND` / `OR` / operators:
|
|
87
|
+
|
|
88
|
+
```graphql
|
|
89
|
+
{
|
|
90
|
+
people(
|
|
91
|
+
where: {
|
|
92
|
+
AND: [
|
|
93
|
+
{ column: AGE, operator: GT, value: 37 }
|
|
94
|
+
{ OR: [
|
|
95
|
+
{ column: TYPE, value: "Actor" }
|
|
96
|
+
{ column: HEIGHT, operator: GTE, value: 150 }
|
|
97
|
+
] }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
) { name }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The generated `SQLOperator` enum covers `EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`,
|
|
105
|
+
`IN`, `NIN`, `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, `IS_NULL`, `IS_NOT_NULL`.
|
|
106
|
+
|
|
107
|
+
### `@whereHasConditions`
|
|
108
|
+
|
|
109
|
+
Filter by the existence of a related record, with conditions applied to the
|
|
110
|
+
relation:
|
|
111
|
+
|
|
112
|
+
```graphql
|
|
113
|
+
type Query {
|
|
114
|
+
users(
|
|
115
|
+
hasPosts: _ @whereHasConditions(columns: ["status"])
|
|
116
|
+
): [User!]! @all
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The relation is inferred from the `has<Relation>` argument name (`hasPosts` →
|
|
121
|
+
`posts`), or specified with `relation:`.
|
|
122
|
+
|
|
123
|
+
## How columns are resolved
|
|
124
|
+
|
|
125
|
+
Column names from enums (e.g. `CREATED_AT`) and arguments (`minAge`) are mapped to
|
|
126
|
+
real columns through one central rule (`Lighthouse::Support::Naming`): ALLCAPS
|
|
127
|
+
enum keys are downcased, and camelCase is converted to snake_case. This keeps
|
|
128
|
+
client-facing GraphQL idiomatic while matching your Rails schema.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide takes you from an empty Rails app to a working, directive-driven
|
|
4
|
+
GraphQL endpoint.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# Gemfile
|
|
10
|
+
gem 'lighthouse-graphql'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```shell
|
|
14
|
+
bundle install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Runtime dependencies (`graphql ~> 2.5`, `activesupport`) come with the gem. If you
|
|
18
|
+
want federation, add `apollo-federation` to your app — it is intentionally **not**
|
|
19
|
+
a hard dependency of Lighthouse so non-federated apps stay lean.
|
|
20
|
+
|
|
21
|
+
## 1. Write your schema (SDL)
|
|
22
|
+
|
|
23
|
+
By default Lighthouse loads every `*.graphql` file under `RAILS_ROOT/graphql`,
|
|
24
|
+
sorted by path and concatenated. Split your schema however you like:
|
|
25
|
+
|
|
26
|
+
```graphql
|
|
27
|
+
# graphql/schema.graphql
|
|
28
|
+
type Query {
|
|
29
|
+
users: [User!]! @all
|
|
30
|
+
user(id: ID! @eq): User @find
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type User {
|
|
34
|
+
id: ID!
|
|
35
|
+
name: String
|
|
36
|
+
email: String
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`@all` resolves to `User.all`; `@find` resolves a single `User` by the `id`
|
|
41
|
+
argument (the `@eq` makes `id` a filter). Lighthouse infers the model from the
|
|
42
|
+
field's return type name (`User`), or you can be explicit with
|
|
43
|
+
`@all(model: "User")`.
|
|
44
|
+
|
|
45
|
+
## 2. Provide your base types
|
|
46
|
+
|
|
47
|
+
Lighthouse builds on graphql-ruby, so you supply the base classes it should use
|
|
48
|
+
for objects, interfaces, etc. — exactly the place to mix in Apollo Federation or
|
|
49
|
+
any custom field/argument behavior.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# app/graphql/base_object.rb
|
|
53
|
+
class BaseObject < GraphQL::Schema::Object
|
|
54
|
+
# include ApolloFederation::Object # if you use federation
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 3. Build the schema
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# app/graphql/app_schema.rb
|
|
62
|
+
require 'lighthouse-graphql'
|
|
63
|
+
|
|
64
|
+
AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
65
|
+
sdl_folder: Rails.root.join('graphql').to_s,
|
|
66
|
+
options: {
|
|
67
|
+
base_types: {
|
|
68
|
+
object: BaseObject,
|
|
69
|
+
# interface:, union:, enum:, input_object:, scalar: ...
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`get_schema` returns a `GraphQL::Schema` subclass you can use directly, or
|
|
76
|
+
subclass further (e.g. to add federation, tracing, a Dataloader):
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
class AppSchema < AppSchema # or build a Base*Schema and subclass it
|
|
80
|
+
use GraphQL::Dataloader
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> Lighthouse uses graphql-ruby's `Dataloader` to batch relationship loads. Add
|
|
85
|
+
> `use GraphQL::Dataloader` to your schema to get N+1-free relationships.
|
|
86
|
+
|
|
87
|
+
## 4. Serve it
|
|
88
|
+
|
|
89
|
+
A standard graphql-ruby controller:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class GraphqlController < ApplicationController
|
|
93
|
+
def execute
|
|
94
|
+
result = AppSchema.execute(
|
|
95
|
+
params[:query],
|
|
96
|
+
variables: params[:variables],
|
|
97
|
+
context: { current_user: current_user },
|
|
98
|
+
operation_name: params[:operationName]
|
|
99
|
+
)
|
|
100
|
+
render json: result
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 5. Configure (optional)
|
|
106
|
+
|
|
107
|
+
Create an initializer to wire app-specific settings. This is also the single
|
|
108
|
+
place to register custom directives and federation reference resolvers, which
|
|
109
|
+
keeps Lighthouse itself free of any app constants.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# config/initializers/lighthouse.rb
|
|
113
|
+
require 'lighthouse-graphql'
|
|
114
|
+
|
|
115
|
+
Lighthouse.configure do |config|
|
|
116
|
+
# Namespaces tried when resolving a class referenced by a directive argument,
|
|
117
|
+
# e.g. @field(resolver: "Users::Profile") or @all(model: "User").
|
|
118
|
+
config.resolver_namespaces = ['App::Graphql', 'Resolvers']
|
|
119
|
+
|
|
120
|
+
# How swallowed directive errors are surfaced (raise in test, log otherwise).
|
|
121
|
+
# config.on_error = ->(error, _ctx) { Sentry.capture_exception(error) }
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
See [configuration.md](configuration.md) for all options.
|
|
126
|
+
|
|
127
|
+
## Casing: camelCase ↔ snake_case
|
|
128
|
+
|
|
129
|
+
GraphQL conventionally uses `camelCase` field names; Rails uses `snake_case`
|
|
130
|
+
attributes. Lighthouse bridges them with one rule: a field with no directive is
|
|
131
|
+
read off the parent object by its **snake_cased** name.
|
|
132
|
+
|
|
133
|
+
```graphql
|
|
134
|
+
type User {
|
|
135
|
+
customAttributes: String # reads user.custom_attributes
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
When the mapping isn't a simple `underscore`, use [`@rename`](directives.md#rename):
|
|
140
|
+
|
|
141
|
+
```graphql
|
|
142
|
+
type User {
|
|
143
|
+
displayName: String @rename(attribute: "full_name")
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Next steps
|
|
148
|
+
|
|
149
|
+
- [Directives reference](directives.md) — the full vocabulary.
|
|
150
|
+
- [Relationships](relationships.md) — wiring associations.
|
|
151
|
+
- [Filtering & ordering](filtering-and-ordering.md) — query constraints.
|
|
152
|
+
- [Apollo Federation](federation.md) — become a subgraph.
|