lighthouse-graphql 0.3.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +182 -1
- data/README.md +18 -7
- data/docs/best-practices.md +16 -6
- data/docs/configuration.md +33 -8
- data/docs/directives.md +318 -10
- data/docs/events.md +3 -1
- data/docs/federation.md +3 -2
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +66 -32
- data/docs/motivation.md +3 -2
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +338 -25
- data/docs/security.md +227 -0
- data/docs/subscriptions.md +60 -37
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +70 -0
- data/lib/generators/lighthouse/class_generator.rb +19 -8
- data/lib/generators/lighthouse/install/install_generator.rb +96 -20
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
- data/lib/generators/lighthouse/mutation_generator.rb +5 -4
- data/lib/generators/lighthouse/query_generator.rb +5 -4
- data/lib/generators/lighthouse/subscription_generator.rb +21 -16
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +175 -0
- data/lib/lighthouse/complexity_analyzer.rb +56 -0
- data/lib/lighthouse/config.rb +16 -4
- data/lib/lighthouse/controller.rb +27 -9
- data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
- data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
- data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
- data/lib/lighthouse/directive_resolvers/base.rb +63 -14
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
- data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
- data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
- data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
- data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
- data/lib/lighthouse/directives/definitions.rb +52 -0
- data/lib/lighthouse/directives/model_directive.rb +131 -0
- data/lib/lighthouse/directives/node_directive.rb +134 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
- data/lib/lighthouse/directives/registry.rb +21 -20
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -0
- data/lib/lighthouse/events/execution_trace.rb +2 -1
- data/lib/lighthouse/events/lifecycle.rb +3 -1
- data/lib/lighthouse/field_resolver_stack.rb +59 -11
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +31 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/multipart_request.rb +43 -0
- data/lib/lighthouse/node_registry.rb +68 -0
- data/lib/lighthouse/pagination.rb +3 -0
- data/lib/lighthouse/rb_lighthouse.rb +36 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +68 -0
- data/lib/lighthouse/resolver_dispatch.rb +36 -11
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_factory.rb +0 -1
- data/lib/lighthouse/schema_generator.rb +21 -12
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/sdl_loader.rb +0 -2
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +8 -6
- data/lib/lighthouse/support/preload.rb +49 -0
- data/lib/lighthouse/type.rb +103 -0
- data/lib/lighthouse/type_resolution.rb +150 -0
- data/lib/lighthouse/upload.rb +26 -0
- data/lib/lighthouse/upload_store.rb +32 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse/graphql.rbs +29 -0
- data/sig/lighthouse.rbs +18 -0
- metadata +43 -1
data/docs/directives.md
CHANGED
|
@@ -129,6 +129,7 @@ See [relationships.md](relationships.md) for the full guide.
|
|
|
129
129
|
| `@belongsTo` | inverse of has-one/has-many |
|
|
130
130
|
| `@belongsToMany` | many-to-many: a list, or paginated with `type:` |
|
|
131
131
|
| `@morphTo`, `@morphOne`, `@morphMany`, `@morphToMany` | the polymorphic variants |
|
|
132
|
+
| `@hasManyThrough`, `@hasOneThrough` | PHP Lighthouse's names for `@hasMany` / `@hasOne` on a `has_many` / `has_one :through` |
|
|
132
133
|
|
|
133
134
|
```graphql
|
|
134
135
|
type User {
|
|
@@ -142,6 +143,28 @@ type User {
|
|
|
142
143
|
The relation name defaults to the field name (snake_cased). Override with
|
|
143
144
|
`@hasMany(relation: "authored_posts")`.
|
|
144
145
|
|
|
146
|
+
### `@with`, `@withCount` and `@lazyLoad`
|
|
147
|
+
|
|
148
|
+
Eager loading for fields the relationship directives do not resolve, each across
|
|
149
|
+
every parent in the response at once. See
|
|
150
|
+
[relationships.md](relationships.md#eager-loading-for-other-fields).
|
|
151
|
+
|
|
152
|
+
```graphql
|
|
153
|
+
type User {
|
|
154
|
+
"@with loads the relation before the field resolves, for a method that reads it."
|
|
155
|
+
taskSummary: String! @with(relation: "tasks") @method(name: "task_summary")
|
|
156
|
+
|
|
157
|
+
"@withCount sets posts_count on the user; the field's resolver reads it."
|
|
158
|
+
activity: String! @withCount(relation: "posts", scopes: ["published"]) @method
|
|
159
|
+
|
|
160
|
+
"@lazyLoad loads relations of the records the field returns."
|
|
161
|
+
posts: [Post!]! @hasMany @lazyLoad(relations: ["tags", "comments.author"])
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`relation` / `relations` may be nested with dots. `scopes` narrow the loaded or
|
|
166
|
+
counted records. `@with`'s `relation` defaults to the field's name.
|
|
167
|
+
|
|
145
168
|
---
|
|
146
169
|
|
|
147
170
|
## Field directives
|
|
@@ -150,8 +173,9 @@ The relation name defaults to the field name (snake_cased). Override with
|
|
|
150
173
|
|
|
151
174
|
Delegate a field to a class — your escape hatch for anything the built-in
|
|
152
175
|
directives don't cover. On `Query` and `Mutation` you usually don't need it:
|
|
153
|
-
a field without a resolving directive resolves to `
|
|
154
|
-
`Mutations::<FieldName>`, graphql-ruby resolver and mutation classes
|
|
176
|
+
a field without a resolving directive resolves to `Resolvers::<FieldName>` /
|
|
177
|
+
`Mutations::<FieldName>`, graphql-ruby resolver and mutation classes under
|
|
178
|
+
`Resolvers::BaseResolver` / `Mutations::BaseMutation`. See
|
|
155
179
|
[resolvers.md](resolvers.md).
|
|
156
180
|
|
|
157
181
|
```graphql
|
|
@@ -161,10 +185,16 @@ type Contact {
|
|
|
161
185
|
```
|
|
162
186
|
|
|
163
187
|
On `Query` and `Mutation` the name is looked up in the queries / mutations
|
|
164
|
-
namespaces,
|
|
188
|
+
namespaces, a nested name too (`Feeds::Home` is `Resolvers::Feeds::Home`), and
|
|
189
|
+
then as written. On any other type it is looked up in the resolvers namespace the
|
|
190
|
+
same way (`Posts::TagsCount` is `Resolvers::Posts::TagsCount`), then in
|
|
191
|
+
[`resolver_namespaces`](configuration.md). There, the class's `object` is the parent: the
|
|
192
|
+
record the field belongs to (see
|
|
193
|
+
[resolvers.md](resolvers.md#fields-of-other-types)). The class may
|
|
165
194
|
be a `GraphQL::Schema::Resolver` (graphql-ruby's contract), or a plain callable —
|
|
166
195
|
a class-level `call`, an instance `#call`, or an instance `#resolve`, each
|
|
167
|
-
receiving `(object, args, context)
|
|
196
|
+
receiving `(object, args, context)`, and the [`ResolveInfo`](resolvers.md#resolveinfo)
|
|
197
|
+
as a fourth argument when it takes one:
|
|
168
198
|
|
|
169
199
|
```ruby
|
|
170
200
|
module Resolvers
|
|
@@ -176,6 +206,17 @@ module Resolvers
|
|
|
176
206
|
end
|
|
177
207
|
```
|
|
178
208
|
|
|
209
|
+
A method after `@` replaces `call`, as PHP Lighthouse's `Class@method`: it is called
|
|
210
|
+
on the class when the class has it, else on an instance. On a
|
|
211
|
+
`GraphQL::Schema::Resolver` it replaces `resolve`, and `ready?` / `authorized?`
|
|
212
|
+
still run.
|
|
213
|
+
|
|
214
|
+
```graphql
|
|
215
|
+
type Query {
|
|
216
|
+
usersTotal: Int @field(resolver: "Statistics@users_total") # Resolvers::Statistics.users_total
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
179
220
|
### `@rename`
|
|
180
221
|
|
|
181
222
|
Read a differently-named attribute off the parent object. This is the explicit
|
|
@@ -190,15 +231,157 @@ type User {
|
|
|
190
231
|
### `@method`
|
|
191
232
|
|
|
192
233
|
Resolve a field by calling a method on the parent object (PHP-parity). Great for
|
|
193
|
-
exposing a computed method or predicate without a custom resolver.
|
|
234
|
+
exposing a computed method or predicate without a custom resolver. `name:`
|
|
235
|
+
defaults to the field's snake_case name.
|
|
194
236
|
|
|
195
237
|
```graphql
|
|
196
238
|
type Conversation {
|
|
197
239
|
labels: [String!]! @method(name: "label_list")
|
|
198
240
|
canReply: Boolean @method(name: "can_reply?")
|
|
241
|
+
summary(words: Int = 20): String! @method
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The field's arguments reach the method: as snake_case keywords when it takes
|
|
246
|
+
keywords (`def summary(words:)`), otherwise positionally, in the order the SDL
|
|
247
|
+
declares them, as PHP Lighthouse passes them (`def summary(words)`). The SDL's
|
|
248
|
+
defaults apply, and a method taking no parameters is called without them.
|
|
249
|
+
|
|
250
|
+
## Relay
|
|
251
|
+
|
|
252
|
+
### `@globalId` and `@node`
|
|
253
|
+
|
|
254
|
+
`@globalId` encodes a field's value as a Relay global id (base64 of `Type:id`), and
|
|
255
|
+
decodes an argument or input field (`decode: ARRAY | TYPE | ID`). `@node` on a type
|
|
256
|
+
makes it implement `Node` and adds `Query.node(id:)`, which finds the type's model
|
|
257
|
+
record, or calls `@node(resolver:)`. See [relay.md](relay.md).
|
|
258
|
+
|
|
259
|
+
```graphql
|
|
260
|
+
type User @node {
|
|
261
|
+
id: ID! @globalId
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type Query {
|
|
265
|
+
user(id: ID! @globalId(decode: ID) @eq): User @find
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Execution
|
|
270
|
+
|
|
271
|
+
### `@namespaced`
|
|
272
|
+
|
|
273
|
+
A field that only groups others: it resolves to `true`, and the fields of its type
|
|
274
|
+
resolve with their own directives. On `Query` or `Mutation`.
|
|
275
|
+
|
|
276
|
+
```graphql
|
|
277
|
+
type Query { post: PostQueries! @namespaced }
|
|
278
|
+
type PostQueries {
|
|
279
|
+
find(id: ID! @eq): Post @find
|
|
280
|
+
list: [Post!]! @paginate
|
|
199
281
|
}
|
|
200
282
|
```
|
|
201
283
|
|
|
284
|
+
### `@throttle`
|
|
285
|
+
|
|
286
|
+
Rate-limits a field per client: at most `maxAttempts` calls every `decayMinutes`
|
|
287
|
+
(default 60 a minute), counted in [`cache_store`](configuration.md#cache_store). A
|
|
288
|
+
client is the signed-in user, else the context's `:remote_ip`, which the controller
|
|
289
|
+
concern sets. Past the limit the field fails with `Rate limit for Query.x exceeded.
|
|
290
|
+
Try again later.` before anything else runs. `name:` makes fields count together,
|
|
291
|
+
`prefix:` separates groups.
|
|
292
|
+
|
|
293
|
+
```graphql
|
|
294
|
+
type Mutation {
|
|
295
|
+
login(email: String!, password: String!): User @throttle(maxAttempts: 5)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### `@async`
|
|
300
|
+
|
|
301
|
+
Runs a mutation in a job and answers `true` at once. `@guard`, the `@can*` family,
|
|
302
|
+
`@validator` and the argument directives run in the request, so the client still
|
|
303
|
+
gets their errors; what resolves the field runs in
|
|
304
|
+
`Lighthouse::GraphQL::AsyncMutationJob` (ActiveJob), with the arguments and the
|
|
305
|
+
context values ActiveJob can serialize (records too, through GlobalID). The field
|
|
306
|
+
must be a `Mutation` field returning `Boolean`, and the schema a named constant.
|
|
307
|
+
|
|
308
|
+
```graphql
|
|
309
|
+
type Mutation {
|
|
310
|
+
importFeed(url: String!): Boolean! @guard @async(queue: "imports")
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### `@hide` and `@show`
|
|
315
|
+
|
|
316
|
+
Leave a field out of the schema by environment (`Rails.env`): `@hide(env:)` removes
|
|
317
|
+
it in the environments listed, `@show(env:)` keeps it only in those.
|
|
318
|
+
|
|
319
|
+
```graphql
|
|
320
|
+
type Query {
|
|
321
|
+
debugInfo: String @show(env: ["development", "test"])
|
|
322
|
+
legacyFeed: [Post!]! @hide(env: ["production"]) @all
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Type directives
|
|
327
|
+
|
|
328
|
+
### `@model`
|
|
329
|
+
|
|
330
|
+
Maps an object type to the ActiveRecord model whose records it resolves, when the
|
|
331
|
+
names differ, as PHP Lighthouse's. Every directive that takes `model:` (`@all`,
|
|
332
|
+
`@find`, `@first`, `@paginate`, `@create`, `@update`, `@upsert`, `@delete`, the
|
|
333
|
+
`@can*` family...) on a field returning the type gets it, unless it names its own; a
|
|
334
|
+
union or interface resolves the model's records to the type; `@node` finds them. The
|
|
335
|
+
schema does not build unless the class is an ActiveRecord model.
|
|
336
|
+
|
|
337
|
+
```graphql
|
|
338
|
+
type Article @model(class: "Post") { id: ID! title: String! }
|
|
339
|
+
|
|
340
|
+
type Query {
|
|
341
|
+
articles: [Article!]! @paginate # pages through Post
|
|
342
|
+
article(id: ID! @eq): Article @find
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### `@enum`
|
|
347
|
+
|
|
348
|
+
The internal value of an enum value: what a resolver gets for it in an argument,
|
|
349
|
+
and what a result answers with it for.
|
|
350
|
+
|
|
351
|
+
```graphql
|
|
352
|
+
enum Role {
|
|
353
|
+
ADMIN @enum(value: "admin")
|
|
354
|
+
MEMBER @enum(value: "member")
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
type Query { users(role: Role @eq): [User!]! @all } # filters on "admin"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
They go on a union, an interface or a scalar, and name the class that resolves or
|
|
361
|
+
coerces its values. Without them, Lighthouse uses the type's class
|
|
362
|
+
(`Types::<Name>Type` including `Lighthouse::GraphQL::Type`), or its default. See
|
|
363
|
+
[types.md](types.md).
|
|
364
|
+
|
|
365
|
+
### `@union` and `@interface`
|
|
366
|
+
|
|
367
|
+
Name the class that picks the object type of each value: `resolve_type(object,
|
|
368
|
+
context)` or `call(object, context)`, returning a type name or a type class.
|
|
369
|
+
Looked up in the types namespace (nested names too), then as written.
|
|
370
|
+
|
|
371
|
+
```graphql
|
|
372
|
+
union SearchResult @union(resolveType: "SearchResultResolver") = Post | User
|
|
373
|
+
interface Authored @interface(resolveType: "AuthoredResolver") { id: ID! }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `@scalar`
|
|
377
|
+
|
|
378
|
+
Names the class that reads and writes the scalar's values: `coerce_input(value,
|
|
379
|
+
context)` and `coerce_result(value, context)`, as a `GraphQL::Schema::Scalar`.
|
|
380
|
+
|
|
381
|
+
```graphql
|
|
382
|
+
scalar DateTime @scalar(class: "Types::DateTimeType")
|
|
383
|
+
```
|
|
384
|
+
|
|
202
385
|
---
|
|
203
386
|
|
|
204
387
|
## Authentication & Authorization
|
|
@@ -262,7 +445,40 @@ type Query {
|
|
|
262
445
|
```
|
|
263
446
|
|
|
264
447
|
Builders are looked up in their own namespace, `Builders::` (app/graphql/builders)
|
|
265
|
-
— they are not resolvers. See [resolvers.md](resolvers.md#builders).
|
|
448
|
+
— they are not resolvers. See [resolvers.md](resolvers.md#builders). The class is
|
|
449
|
+
`class:` with `method:` (default `call`), or PHP Lighthouse's `method: "Class@method"`.
|
|
450
|
+
|
|
451
|
+
On a field, the method gets `(relation, args, context)`, or the directive's
|
|
452
|
+
`value:` in place of the arguments. On an argument or an input field, it gets
|
|
453
|
+
`(relation, value, context)` whenever the client sends that argument, an explicit
|
|
454
|
+
`null` included, and not when it is left out:
|
|
455
|
+
|
|
456
|
+
```graphql
|
|
457
|
+
type Query {
|
|
458
|
+
posts(since: DateTime @builder(method: "Recent@since")): [Post!]! @all
|
|
459
|
+
latest: [Post!]! @all @builder(class: "Recent", value: 5)
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
class Builders::Recent
|
|
465
|
+
def self.call(relation, count, _context) = relation.order(created_at: :desc).limit(count)
|
|
466
|
+
def self.since(relation, time, _context) = time ? relation.where(created_at: time..) : relation
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### `@whereAuth`
|
|
471
|
+
|
|
472
|
+
Keep only the records the signed-in user owns, through a relation of the model: a
|
|
473
|
+
`belongs_to`, a `has_many`, a `:through`. A guest gets none. It applies where
|
|
474
|
+
`@builder` does (`@all`, `@paginate`, `@first`, `@count`, `enhance_builder`).
|
|
475
|
+
|
|
476
|
+
```graphql
|
|
477
|
+
type Query {
|
|
478
|
+
myPosts: [Post!]! @paginate @whereAuth(relation: "author")
|
|
479
|
+
myTeams: [Team!]! @all @whereAuth(relation: "members")
|
|
480
|
+
}
|
|
481
|
+
```
|
|
266
482
|
|
|
267
483
|
```ruby
|
|
268
484
|
module Builders
|
|
@@ -291,6 +507,10 @@ Full guide: [filtering-and-ordering.md](filtering-and-ordering.md).
|
|
|
291
507
|
| `@like` | `column LIKE %value%` |
|
|
292
508
|
| `@whereKey` | `primary key = value` (`IN` for a list) |
|
|
293
509
|
| `@where(operator:)` | generic operator (`GT`, `GTE`, `IN`, `IS_NULL`, `CONTAINS`, …) |
|
|
510
|
+
| `@whereNull` / `@whereNotNull` | `true`: `column IS NULL` / `IS NOT NULL`; `false`: the opposite |
|
|
511
|
+
| `@whereBetween` / `@whereNotBetween` | `column BETWEEN` / `NOT BETWEEN` the input's two fields, in order |
|
|
512
|
+
| `@whereJsonContains` | a JSON column contains the value (every value of a list); `key: "meta->tags"` points into it |
|
|
513
|
+
| `@scope(name:)` | calls the model scope with the value (`name:` defaults to the argument's) |
|
|
294
514
|
| `@orderBy` | sort by one or more columns |
|
|
295
515
|
|
|
296
516
|
```graphql
|
|
@@ -306,6 +526,41 @@ type Query {
|
|
|
306
526
|
|
|
307
527
|
The column defaults to the snake_cased argument name; override with `key:`.
|
|
308
528
|
|
|
529
|
+
```graphql
|
|
530
|
+
type Query {
|
|
531
|
+
posts(
|
|
532
|
+
unpublished: Boolean @whereNull(key: "published_at")
|
|
533
|
+
createdAt: DateRange @whereBetween(key: "created_at")
|
|
534
|
+
tagged: [String!] @whereJsonContains(key: "tags")
|
|
535
|
+
trending: Boolean @scope
|
|
536
|
+
filter: PostFilter @nest
|
|
537
|
+
limit: Int @limit
|
|
538
|
+
): [Post!]! @all
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
input DateRange { from: DateTime! to: DateTime! }
|
|
542
|
+
input PostFilter { title: String @like, authorId: ID @eq(key: "author_id") }
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### `@nest`
|
|
546
|
+
|
|
547
|
+
On an argument whose type is an input object, applies the filters of the input's
|
|
548
|
+
fields (at any depth, for those that carry `@nest` too), as if they were the
|
|
549
|
+
field's own arguments.
|
|
550
|
+
|
|
551
|
+
### `@limit`
|
|
552
|
+
|
|
553
|
+
On an `Int` argument, caps how many results the field returns at the client's
|
|
554
|
+
value; `null` leaves them alone. A query not run yet gets a `LIMIT`; the records of
|
|
555
|
+
a relation, loaded for every parent at once, are cut per parent. A negative value is
|
|
556
|
+
an error.
|
|
557
|
+
|
|
558
|
+
`@limit(builder: true)` puts the `LIMIT` on the field's query instead, with the
|
|
559
|
+
other argument directives. Use it on root fields: on a relation batched for many
|
|
560
|
+
parents it would cut them all together, as PHP Lighthouse warns. PHP Lighthouse
|
|
561
|
+
also allows `@limit` on the field itself, where it only marks the field the argument
|
|
562
|
+
limits; it is accepted there and changes nothing.
|
|
563
|
+
|
|
309
564
|
### `@whereConditions` / `@whereHasConditions`
|
|
310
565
|
|
|
311
566
|
Give clients a structured, dynamic `WHERE` builder (AND/OR/operators, and HAS for
|
|
@@ -318,10 +573,39 @@ type Query {
|
|
|
318
573
|
}
|
|
319
574
|
```
|
|
320
575
|
|
|
576
|
+
### `@trim`, `@hash`, `@drop`, `@bind` and `@upload`
|
|
577
|
+
|
|
578
|
+
Rewrite an argument's or input field's value before the field resolves, in the
|
|
579
|
+
order the SDL lists them:
|
|
580
|
+
|
|
581
|
+
| Directive | The resolver gets |
|
|
582
|
+
| --- | --- |
|
|
583
|
+
| `@trim` | the string without leading and trailing whitespace. On a field: every string it takes, nested inputs included |
|
|
584
|
+
| `@drop` | nothing: the argument is removed |
|
|
585
|
+
| `@hash` | the value hashed with BCrypt (the `bcrypt` gem, as `has_secure_password`) |
|
|
586
|
+
| `@bind(class:, column: "id", with: [], optional: false)` | the model record(s) the value identifies, eager-loading `with`; or what a class's `call(value, context)` returns |
|
|
587
|
+
| `@upload(disk:, path: "/")` | what [`upload_store`](configuration.md#upload_store) answers for the uploaded file: the signed id of an ActiveStorage blob, by default ([uploads.md](uploads.md)) |
|
|
588
|
+
|
|
589
|
+
```graphql
|
|
590
|
+
type Mutation {
|
|
591
|
+
signUp(email: String! @trim, password: String! @hash, captcha: String @drop): User! @create
|
|
592
|
+
assign(task: ID! @bind(class: "Task"), user: ID! @bind(class: "User", with: ["roles"])): Task!
|
|
593
|
+
updateAvatar(avatar: Upload! @upload(path: "avatars")): User! @update @inject(context: "current_user.id", name: "id")
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
`@bind` fails the field with a validation error on the argument (`The selected task
|
|
598
|
+
is invalid.`) when no record matches, unless `optional: true`, which passes `null`.
|
|
599
|
+
|
|
600
|
+
`@globalId`, `@trim` and `@drop` only rewrite values: they run first, so `@guard`,
|
|
601
|
+
the `@can*` family and `@validator` see the result. `@hash`, `@bind` and `@upload`
|
|
602
|
+
run once those have let the call through: a validator sees the password before it
|
|
603
|
+
is hashed, and nothing is stored or looked up for a refused call.
|
|
604
|
+
|
|
321
605
|
### `@convertEmptyStringsToNull`
|
|
322
606
|
|
|
323
607
|
Replaces incoming empty strings `""` with `null`, before any resolver sees them —
|
|
324
|
-
directive resolvers (`@create`, `@all` filters, …), `@field`, `
|
|
608
|
+
directive resolvers (`@create`, `@all` filters, …), `@field`, `Resolvers::` /
|
|
325
609
|
`Mutations::` classes and `@validator` alike. On an argument or an input field it
|
|
326
610
|
converts that one input; on a field it converts every input of the field, nested
|
|
327
611
|
input objects and lists included. Non-null inputs (`String!`) pass unchanged when
|
|
@@ -520,9 +804,9 @@ directive @subscription(class: String!) on FIELD_DEFINITION
|
|
|
520
804
|
|
|
521
805
|
Names the class a `Subscription` field resolves to, when it does not follow the
|
|
522
806
|
convention (`postUpdated` → `Subscriptions::PostUpdated`). The class inherits from
|
|
523
|
-
`GraphQL::Schema::Subscription`
|
|
524
|
-
|
|
525
|
-
`Config.namespaces[:subscriptions]`; a name with `::` is used as it is.
|
|
807
|
+
`Subscriptions::BaseSubscription`, a `GraphQL::Schema::Subscription` that includes
|
|
808
|
+
`Lighthouse::GraphQL::Subscription` (PHP Lighthouse's `filter?` hooks), and is looked
|
|
809
|
+
up in `Config.namespaces[:subscriptions]`; a name with `::` is used as it is.
|
|
526
810
|
|
|
527
811
|
```graphql
|
|
528
812
|
type Subscription {
|
|
@@ -634,6 +918,30 @@ Flipper.enable_actor(:new_checkout, User.find(1))
|
|
|
634
918
|
|
|
635
919
|
---
|
|
636
920
|
|
|
921
|
+
## Query complexity
|
|
922
|
+
|
|
923
|
+
### `@complexity`
|
|
924
|
+
|
|
925
|
+
Customize what a field costs in the query's complexity score, which graphql-ruby
|
|
926
|
+
checks against the schema's `max_complexity` before it runs the query.
|
|
927
|
+
|
|
928
|
+
```graphql
|
|
929
|
+
type Query {
|
|
930
|
+
search(first: Int, term: String!): [Post!]! @complexity
|
|
931
|
+
posts(includeFullText: Boolean): [Post!]! @all @complexity(resolver: "PostsComplexity")
|
|
932
|
+
}
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
| Argument | Meaning |
|
|
936
|
+
| --- | --- |
|
|
937
|
+
| `resolver: String` | A `GraphQL::Analysis::Analyzer` subclass in `namespaces[:analyzers]` (`Analyzers::PostsComplexity`), installed on the schema. With `Lighthouse::GraphQL::ComplexityAnalyzer` included, its `complexity` (a number, or a proc of `context, args, child_complexity`) is the field's cost. Without it, or without `resolver:`: `1 + child complexity × first`. |
|
|
938
|
+
|
|
939
|
+
Paginated fields cost `1 + child complexity × first` without the directive, and a
|
|
940
|
+
field resolved by a `GraphQL::Schema::Resolver` class costs what that class
|
|
941
|
+
declares. See [security.md](security.md).
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
637
945
|
## Events
|
|
638
946
|
|
|
639
947
|
`@event` dispatches an event once the field has resolved. The class is built with
|
data/docs/events.md
CHANGED
|
@@ -107,7 +107,9 @@ end
|
|
|
107
107
|
# => { "data" => {...}, "extensions" => { "queries" => 3 } }
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
`query_complexity` is
|
|
110
|
+
`query_complexity` is the query's complexity score, the number graphql-ruby
|
|
111
|
+
checks against `max_complexity` (see [security.md](security.md#reading-the-score)).
|
|
112
|
+
It is `nil` for a query that failed validation.
|
|
111
113
|
|
|
112
114
|
**`ManipulateResult`**: change `event.result` (the Hash with `data`, `errors` and
|
|
113
115
|
`extensions`) in place, or assign a new Hash to `event.result=`:
|
data/docs/federation.md
CHANGED
|
@@ -13,7 +13,8 @@ Include the federation mixins in the base types you pass to Lighthouse, and make
|
|
|
13
13
|
your schema a federation schema:
|
|
14
14
|
|
|
15
15
|
```ruby
|
|
16
|
-
|
|
16
|
+
# app/graphql/types/base_object.rb
|
|
17
|
+
class Types::BaseObject < GraphQL::Schema::Object
|
|
17
18
|
include ApolloFederation::Object
|
|
18
19
|
|
|
19
20
|
# Default entity resolution: look the record up by id on the model that shares
|
|
@@ -29,7 +30,7 @@ end
|
|
|
29
30
|
|
|
30
31
|
BaseAppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
31
32
|
sdl_folder: Rails.root.join('graphql').to_s,
|
|
32
|
-
options: { federation: true, base_types: { object: BaseObject } }
|
|
33
|
+
options: { federation: true, base_types: { object: Types::BaseObject, resolver: Resolvers::BaseResolver } }
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
class AppSchema < BaseAppSchema
|
data/docs/generators.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Generators
|
|
2
|
+
|
|
3
|
+
Lighthouse builds on graphql-ruby, and so do its generators: it writes only what
|
|
4
|
+
graphql-ruby has no generator for, or what an SDL-first schema needs differently.
|
|
5
|
+
Every graphql-ruby app's base classes (`Types::BaseObject`, `Resolvers::BaseResolver`,
|
|
6
|
+
`Mutations::BaseMutation`, ...) come from graphql-ruby's own `graphql:install`.
|
|
7
|
+
|
|
8
|
+
## `lighthouse:install`
|
|
9
|
+
|
|
10
|
+
```shell
|
|
11
|
+
bin/rails generate lighthouse:install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It runs graphql-ruby's install first
|
|
15
|
+
(`graphql:install --no-relay --skip-graphiql --skip-query-logs`), which writes:
|
|
16
|
+
|
|
17
|
+
- `app/graphql/types/base_*.rb`: `BaseObject`, `BaseField`, `BaseArgument`,
|
|
18
|
+
`BaseInterface`, `BaseUnion`, `BaseScalar`, `BaseEnum`, `BaseInputObject`;
|
|
19
|
+
- `app/graphql/resolvers/base_resolver.rb` and `app/graphql/mutations/base_mutation.rb`;
|
|
20
|
+
- its schema class, `Types::QueryType`, `Types::MutationType` and controller, and the
|
|
21
|
+
`POST /graphql` route.
|
|
22
|
+
|
|
23
|
+
Then Lighthouse puts its own on top of what graphql-ruby wrote in that run:
|
|
24
|
+
|
|
25
|
+
| File | What Lighthouse does |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `app/graphql/app_schema.rb` | Replaced by the schema built from the SDL, naming every base class in `base_types` |
|
|
28
|
+
| `app/controllers/graphql_controller.rb` | Replaced by one including `Lighthouse::GraphQL::Controller` |
|
|
29
|
+
| `app/graphql/types/query_type.rb`, `mutation_type.rb` | Removed: the SDL declares the root types |
|
|
30
|
+
| `app/graphql/mutations/base_mutation.rb` | Made a `GraphQL::Schema::Mutation`: the SDL declares a mutation's arguments, where graphql-ruby's `RelayClassicMutation` takes a single `input:` |
|
|
31
|
+
| `app/graphql/subscriptions/base_subscription.rb` | Written: graphql-ruby has no subscription base. It includes `Lighthouse::GraphQL::Subscription` |
|
|
32
|
+
| `config/initializers/lighthouse.rb` | Written: every [configuration](configuration.md) option, commented out |
|
|
33
|
+
| `graphql/schema.graphql` | A starter schema, unless the folder holds `.graphql` files already |
|
|
34
|
+
| `graphql.config.yml` | IDE configuration, and `.gitignore` entries for the [IDE helper](getting-started.md#ide-support) files |
|
|
35
|
+
|
|
36
|
+
| Option | Effect |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `--schema=ApiSchema` | Name of the schema constant (default `AppSchema`) |
|
|
39
|
+
| `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
|
|
40
|
+
| `--skip-controller` | No controller or route (graphql-ruby's are removed too) — for apps that serve GraphQL themselves |
|
|
41
|
+
| `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
|
|
42
|
+
| `--force` | Replace a schema class or controller the app had before |
|
|
43
|
+
|
|
44
|
+
Rerunning it is safe: a file the app has is kept, and the route and `.gitignore`
|
|
45
|
+
entries are added once. In an app that already had a graphql-ruby schema class or
|
|
46
|
+
controller, those are kept (Lighthouse replaces only what graphql-ruby wrote in the
|
|
47
|
+
same run), and graphql-ruby's install points that schema at the root types it
|
|
48
|
+
wrote: have the controller call `AppSchema.execute(...)` to serve Lighthouse's
|
|
49
|
+
schema, or pass `--force`.
|
|
50
|
+
|
|
51
|
+
## Lighthouse's generators
|
|
52
|
+
|
|
53
|
+
For the classes graphql-ruby has no generator for, or none that fits an SDL-first
|
|
54
|
+
schema. Each writes into the first namespace configured for its kind (or
|
|
55
|
+
`--namespace=…`); namespaced names nest modules (`Blog::Publish`).
|
|
56
|
+
|
|
57
|
+
| Generator | Writes |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `lighthouse:query LatestPosts` | `Resolvers::LatestPosts < BaseResolver`, which `Query.latestPosts` resolves to by convention |
|
|
60
|
+
| `lighthouse:mutation PublishPost` | `Mutations::PublishPost < BaseMutation`, for `Mutation.publishPost` |
|
|
61
|
+
| `lighthouse:subscription PostUpdated` | `Subscriptions::PostUpdated < BaseSubscription`, with its hooks (and the base, when missing) |
|
|
62
|
+
| `lighthouse:builder Published` | `Builders::Published`, for `@builder(class: "Published")` |
|
|
63
|
+
| `lighthouse:validator CreatePostInput` | `Validators::CreatePostInputValidator`, a `Dry::Validation::Contract` |
|
|
64
|
+
|
|
65
|
+
The class's arguments and return type are the SDL's, so it declares neither. When
|
|
66
|
+
the base class is missing (an app that never ran an install, or a namespace of its
|
|
67
|
+
own such as `Api::Resolvers`), the generator says so: `lighthouse:install` writes it
|
|
68
|
+
through graphql-ruby, or write `Api::Resolvers::BaseResolver` yourself.
|
|
69
|
+
|
|
70
|
+
## graphql-ruby's generators in a Lighthouse app
|
|
71
|
+
|
|
72
|
+
| Generator | In a Lighthouse app |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `graphql:install` | Run by `lighthouse:install`. On its own, it writes a code-first schema, root types and a `RelayClassicMutation` base: run `lighthouse:install` instead |
|
|
75
|
+
| `graphql:object Post` | **Use it** for a [type class](resolvers.md#type-classes): add `include Lighthouse::GraphQL::Type`, then methods. The `field` lines it writes (from the model's columns, when there is one) are ignored, as the SDL declares the fields: delete them |
|
|
76
|
+
| `graphql:scalar DateTime` | **Use it** for a [scalar](types.md#scalars): add `include Lighthouse::GraphQL::Type`; its `coerce_input` / `coerce_result` are the ones Lighthouse calls |
|
|
77
|
+
| `graphql:union SearchResult`, `graphql:interface Authored` | **Use them** when the [default resolution](types.md#the-default) is not enough: add `include Lighthouse::GraphQL::Type` and a `resolve_type`. Its `possible_types` and `field` lines are ignored |
|
|
78
|
+
| `graphql:enum`, `graphql:input` | Not needed: the SDL declares enums and inputs ([`@enum`](directives.md#enum) gives a value an internal one) |
|
|
79
|
+
| `graphql:mutation`, `graphql:mutation_create` / `_update` / `_delete` | **Not ready**: they add a field to `Types::MutationType`, which a Lighthouse app has none of (they write it again), and write a `RelayClassicMutation` taking `input:`. Use `lighthouse:mutation`, or [`@create`, `@update`, `@delete`](mutations.md) |
|
|
80
|
+
| `graphql:relay` | Not needed: [`@node`, `@globalId`](relay.md) and `@paginate(type: CONNECTION)` |
|
|
81
|
+
| `graphql:loader` | A GraphQL::Batch loader, which your own resolver classes may use; Lighthouse's directives batch through graphql-ruby's Dataloader instead |
|
|
82
|
+
| `graphql:detailed_trace` | **Not ready**: it edits a `class AppSchema < GraphQL::Schema`, which Lighthouse's `AppSchema = get_schema(...)` is not. Add its migration and `AppSchema.use(GraphQL::Tracing::DetailedTrace)` by hand |
|